From: Hannes Laimer <h.laimer@proxmox.com>
To: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>,
pbs-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-backup v5 3/9] datastore: add move_group
Date: Wed, 25 Mar 2026 07:20:30 +0100 [thread overview]
Message-ID: <6633ce9a-6580-4c58-84a5-3f32317d72de@proxmox.com> (raw)
In-Reply-To: <1774354673.7zvzlsmevd.astroid@yuna.none>
On 2026-03-24 13:59, Fabian Grünbichler wrote:
> On March 19, 2026 5:13 pm, Hannes Laimer wrote:
>> BackupGroup::move_to() performs the actual group relocation: for the
>> filesystem backend a single rename(2) moves the group directory
>> atomically. The orphaned group lock file is removed afterwards. For
>> the S3 backend objects under the source group prefix are listed,
>> copied to their destination keys, and then deleted.
>>
>> DataStore::move_group() is the public entry point. It acquires shared
>> namespace locks on both source and target namespaces and an exclusive
>> group lock, validates existence under those locks, ensures the target
>> type directory exists, then calls BackupGroup::move_to().
>>
>> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
>> ---
>> pbs-datastore/src/backup_info.rs | 149 ++++++++++++++++++++++++++++++-
>> pbs-datastore/src/datastore.rs | 49 ++++++++++
>> 2 files changed, 197 insertions(+), 1 deletion(-)
>>
>> diff --git a/pbs-datastore/src/backup_info.rs b/pbs-datastore/src/backup_info.rs
>> index 57e0448f..5931b7b5 100644
>> --- a/pbs-datastore/src/backup_info.rs
>> +++ b/pbs-datastore/src/backup_info.rs
>> @@ -9,7 +9,7 @@ use std::time::Duration;
>> use anyhow::{bail, format_err, Context, Error};
>> use const_format::concatcp;
>>
>> -use proxmox_s3_client::S3PathPrefix;
>> +use proxmox_s3_client::{S3ObjectKey, S3PathPrefix};
>> use proxmox_sys::fs::{lock_dir_noblock, lock_dir_noblock_shared, replace_file, CreateOptions};
>> use proxmox_systemd::escape_unit;
>>
>> @@ -273,6 +273,153 @@ impl BackupGroup {
>> Ok(delete_stats)
>> }
>>
>> + /// Move this group to a new namespace.
>> + ///
>> + /// For the filesystem backend, uses `rename` to atomically relocate the group directory. For
>> + /// the S3 backend, copies all objects to the destination prefix first, then renames the local
>> + /// cache directory, then deletes the source objects. A copy failure returns an error with the
>> + /// group intact at source. A delete failure is logged as a warning - any un-deleted source
>> + /// objects are orphaned and must be removed manually.
>> + ///
>> + /// The caller must have created the target type directory
>> + /// (e.g. `{target_ns}/{backup_type}/`) before calling this method.
>> + ///
>> + /// The caller must hold either an exclusive namespace lock on the source namespace (as in
>> + /// `move_namespace`) or both a shared namespace lock and an exclusive group lock (as in
>> + /// `move_group`). This is required to prevent concurrent writers from adding objects between
>> + /// the S3 copy sweep and the subsequent deletes.
>> + pub(crate) fn move_to(
>> + &self,
>> + target_ns: &BackupNamespace,
>> + backend: &DatastoreBackend,
>> + ) -> Result<(), Error> {
>> + let src_path = self.full_group_path();
>> + let target_path = self.store.group_path(target_ns, &self.group);
>> +
>> + log::info!("moving backup group {src_path:?} to {target_path:?}");
>> +
>> + match backend {
>> + DatastoreBackend::Filesystem => {
>> + std::fs::rename(&src_path, &target_path).with_context(|| {
>> + format!("failed to move group {src_path:?} to {target_path:?}")
>> + })?;
>> + // The caller's lock guard still holds the FD open and will harmlessly fail to
>> + // remove this path when dropped.
>> + let _ = std::fs::remove_file(self.lock_path());
>
> just concentrating on this variant for now..
>
>> + }
>> + DatastoreBackend::S3(s3_client) => {
>
>> [.. thus snipping this ..]
>
>> + }
>> + }
>> +
>> + Ok(())
>> + }
>> +
>> /// Helper function, assumes that no more snapshots are present in the group.
>> fn remove_group_dir(&self) -> Result<(), Error> {
>> let note_path = self.store.group_notes_path(&self.ns, &self.group);
>> diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
>> index 18712074..d4c88452 100644
>> --- a/pbs-datastore/src/datastore.rs
>> +++ b/pbs-datastore/src/datastore.rs
>> @@ -1017,6 +1017,55 @@ impl DataStore {
>> backup_group.destroy(&self.backend()?)
>> }
>>
>> + /// Move a single backup group to a different namespace within the same datastore.
>> + ///
>> + /// Acquires shared namespace locks on both the source and target namespaces, and an exclusive
>> + /// group lock on the source group to prevent concurrent writes to the same group.
>> + pub fn move_group(
>> + self: &Arc<Self>,
>> + source_ns: &BackupNamespace,
>> + group: &pbs_api_types::BackupGroup,
>> + target_ns: &BackupNamespace,
>> + ) -> Result<(), Error> {
>> + if source_ns == target_ns {
>> + bail!("source and target namespace must be different");
>> + }
>> +
>> + let source_group = self.backup_group(source_ns.clone(), group.clone());
>> + let target_group = self.backup_group(target_ns.clone(), group.clone());
>> +
>> + let _src_ns_lock =
>> + lock_namespace_shared(self.name(), source_ns, Some(NS_SHARED_LOCK_TIMEOUT))
>> + .with_context(|| format!("failed to lock source namespace '{source_ns}'"))?;
>> + let _tgt_ns_lock =
>> + lock_namespace_shared(self.name(), target_ns, Some(NS_SHARED_LOCK_TIMEOUT))
>> + .with_context(|| format!("failed to lock target namespace '{target_ns}'"))?;
>> +
>> + let _group_lock = source_group
>> + .lock()
>> + .with_context(|| format!("failed to lock group '{group}' for move"))?;
>
> this breaks concurrently running readers, verification tasks, .. that
> have already started working on a snapshot within that particular group..
>
right.. I had the exclusive ns lock in mind, but you're right, we only
hold that when moving a ns. for single groups we don't, what we could do
tough is lock the group+snapshots exclusively if we only move a single
group. and don't lock for ns moves since we can rely on the exclusive ns
lock. This would only lock the smallest possible scope for both cases
buuut yes, this would still mean up to 8 extra `flock`s per operation..
all of them, regardless of an active move..
on the other hand, flock are cheap
> compare this to deleting a group, which will:
> - obtain an exclusive lock on the group (preventing concurrent snapshot
> additions/removals)
> - obtain an exclusive lock on each snapshot in turn (which will fail if
> there is already a reader/.. task operating on it)
>
> I don't think we can take such shortcuts for this feature, even if it
> means making it a bit more expensive also for the file system case.
>
> but if we can't take such shortcuts.. we don't really benefit from
> namespace locks anymore, since we need to recursively lock and move best
> effort anyway..
>
> we could still implement a fast version that is coupled with a
> maintenance mode (that in turn forbids tasks other than moving), if we
> really want to?
>
>> +
>> + // Check existence under locks to avoid TOCTOU races with concurrent backups or
>> + // namespace operations.
>> + if !self.namespace_exists(target_ns) {
>> + bail!("target namespace '{target_ns}' does not exist");
>> + }
>> + if !source_group.exists() {
>> + bail!("group '{group}' does not exist in namespace '{source_ns}'");
>> + }
>> + if target_group.exists() {
>> + bail!("group '{group}' already exists in target namespace '{target_ns}'");
>> + }
>> +
>> + let backend = self.backend()?;
>> +
>> + std::fs::create_dir_all(self.type_path(target_ns, group.ty)).with_context(|| {
>> + format!("failed to create type directory in '{target_ns}' for move")
>> + })?;
>> +
>> + source_group.move_to(target_ns, &backend)
>> + }
>> +
>> /// Remove a backup directory including all content
>> pub fn remove_backup_dir(
>> self: &Arc<Self>,
>> --
>> 2.47.3
>>
>>
>>
>>
>>
>>
next prev parent reply other threads:[~2026-03-25 6:20 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-19 16:13 [PATCH proxmox-backup v5 0/9] fixes #6195: add support for moving groups and namespaces Hannes Laimer
2026-03-19 16:13 ` [PATCH proxmox-backup v5 1/9] ui: show empty groups Hannes Laimer
2026-03-24 10:28 ` Dominik Csapak
2026-03-19 16:13 ` [PATCH proxmox-backup v5 2/9] datastore: add namespace-level locking Hannes Laimer
2026-03-24 13:00 ` Fabian Grünbichler
2026-03-19 16:13 ` [PATCH proxmox-backup v5 3/9] datastore: add move_group Hannes Laimer
2026-03-24 13:00 ` Fabian Grünbichler
2026-03-25 6:20 ` Hannes Laimer [this message]
2026-03-19 16:13 ` [PATCH proxmox-backup v5 4/9] datastore: add move_namespace Hannes Laimer
2026-03-24 13:00 ` Fabian Grünbichler
2026-03-25 6:32 ` Hannes Laimer
2026-03-25 8:55 ` Fabian Grünbichler
2026-03-19 16:13 ` [PATCH proxmox-backup v5 5/9] api: add PUT endpoint for move_group Hannes Laimer
2026-03-19 16:13 ` [PATCH proxmox-backup v5 6/9] api: add PUT endpoint for move_namespace Hannes Laimer
2026-03-19 16:13 ` [PATCH proxmox-backup v5 7/9] ui: add move group action Hannes Laimer
2026-03-24 10:41 ` Dominik Csapak
2026-03-19 16:13 ` [PATCH proxmox-backup v5 8/9] ui: add move namespace action Hannes Laimer
2026-03-24 10:56 ` Dominik Csapak
2026-03-19 16:13 ` [PATCH proxmox-backup v5 9/9] cli: add move-namespace and move-group commands Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=6633ce9a-6580-4c58-84a5-3f32317d72de@proxmox.com \
--to=h.laimer@proxmox.com \
--cc=f.gruenbichler@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.