all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers
@ 2026-09-15 15:59 Shan Shaji
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper Shan Shaji
                   ` (5 more replies)
  0 siblings, 6 replies; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 15:59 UTC (permalink / raw)
  To: pbs-devel

Sending this as an RFC to get an initial feedback. I'd really appreciate
any suggestions or pointers to anything I may have missed.

Reusing an S3-backed datastore currently restores the backup metadata to
the local cache without creating local chunk markers. A garbage collection
run is then needed to recreate markers for chunks referenced by the backup
indexes.

This series addresses #7908 by listing the chunk objects in S3 and creating
zero-byte local markers when a datastore is recreated with
--reuse-datastore. It also adds an optional rebuild-chunk-markers parameter
to the s3-refresh API and exposes it through proxmox-backup-manager:

    proxmox-backup-manager datastore s3-refresh <store> --rebuild-chunk-markers true

The option is disabled by default for explicit refreshes. When enabled,
it creates markers for all chunk objects with valid chunk keys, including
chunks not referenced by the restored backup indexes. Existing cached
chunk files are replaced with empty markers.

Shan Shaji (4):
  datastore: factor out s3 chunk objects pagination into a helper
  fix #7908: datastore: create zero-byte markers when re-using datastore
  datastore: api: add option to rebuild S3 chunk markers
  datastore: backup-manager: expose option to rebuild s3 chunk markers

 pbs-datastore/src/datastore.rs              | 120 +++++++++++++++-----
 src/api2/admin/datastore.rs                 |  23 +++-
 src/api2/config/datastore.rs                |   6 +-
 src/bin/proxmox_backup_manager/datastore.rs |   7 ++
 4 files changed, 121 insertions(+), 35 deletions(-)

--
2.47.3




^ permalink raw reply	[flat|nested] 9+ messages in thread

* [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
@ 2026-09-15 15:59 ` Shan Shaji
  2026-09-25 12:11   ` Christian Ebner
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore Shan Shaji
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 15:59 UTC (permalink / raw)
  To: pbs-devel

This is in preparation to re-use the chunk iteration logic for creating
empty marker files during s3 refresh.

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 pbs-datastore/src/datastore.rs | 85 ++++++++++++++++++++++------------
 1 file changed, 56 insertions(+), 29 deletions(-)

diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
index c3db52269..f3eab8a28 100644
--- a/pbs-datastore/src/datastore.rs
+++ b/pbs-datastore/src/datastore.rs
@@ -2382,6 +2382,47 @@ impl DataStore {
         }
     }
 
+    /// Fetch chunk objects in batches of max 1000 per request.
+    /// For each objects listed, the `callback` will be invoked with its
+    /// S3 object key and size.
+    fn paginate_s3_chunk_objects<F>(
+        &self,
+        worker: &dyn WorkerTaskContext,
+        s3_client: &S3Client,
+        mut callback: F,
+    ) -> Result<(), Error>
+    where
+        F: FnMut(S3ObjectKey, u64) -> Result<(), Error>,
+    {
+        let prefix = S3PathPrefix::Some(".chunks/".to_string());
+        let mut list_bucket_result =
+            proxmox_async::runtime::block_on(s3_client.list_objects_v2(&prefix, None))
+                .context("failed to list chunk in s3 object store")?;
+
+        loop {
+            for content in list_bucket_result.contents {
+                worker.check_abort()?;
+                worker.fail_on_shutdown()?;
+
+                callback(content.key, content.size)?;
+            }
+
+            // Process next batch of chunks if there are more.
+            if list_bucket_result.is_truncated {
+                list_bucket_result = proxmox_async::runtime::block_on(s3_client.list_objects_v2(
+                    &prefix,
+                    list_bucket_result.next_continuation_token.as_deref(),
+                ))?;
+
+                continue;
+            }
+
+            break;
+        }
+
+        Ok(())
+    }
+
     fn garbage_collection_impl(
         &self,
         worker: &dyn WorkerTaskContext,
@@ -2473,28 +2514,23 @@ impl DataStore {
 
         if let Some(ref s3_client) = s3_client {
             let mut chunk_count = 0;
-            let prefix = S3PathPrefix::Some(".chunks/".to_string());
-            // Operates in batches of 1000 objects max per request
-            let mut list_bucket_result =
-                proxmox_async::runtime::block_on(s3_client.list_objects_v2(&prefix, None))
-                    .context("failed to list chunk in s3 object store")?;
-
             let mut delete_list =
                 S3DeleteList::with_thresholds(S3_DELETE_BATCH_LIMIT, S3_DELETE_DEFER_LIMIT_SECONDS);
-            loop {
-                for content in list_bucket_result.contents {
-                    worker.check_abort()?;
-                    worker.fail_on_shutdown()?;
+
+            self.paginate_s3_chunk_objects(
+                worker,
+                s3_client,
+                |object_key, size| -> Result<(), Error> {
                     let (chunk_path, digest, bad) =
-                        match self.chunk_path_from_object_key(&content.key) {
+                        match self.chunk_path_from_object_key(&object_key) {
                             Some(path) => path,
-                            None => continue,
+                            None => return Ok(()),
                         };
 
                     let timeout = std::time::Duration::from_secs(0);
                     let _chunk_guard = match self.inner.chunk_store.lock_chunk(&digest, timeout) {
                         Ok(guard) => guard,
-                        Err(_) => continue,
+                        Err(_) => return Ok(()),
                     };
                     let _guard = self.inner.chunk_store.mutex().lock().unwrap();
 
@@ -2526,7 +2562,7 @@ impl DataStore {
                                 atime,
                                 min_atime,
                                 oldest_writer,
-                                content.size,
+                                size,
                                 bad,
                                 &mut gc_status,
                                 || {
@@ -2537,15 +2573,15 @@ impl DataStore {
                                             std::fs::remove_file(chunk_path)?;
                                         }
                                     }
-                                    delete_list.push(content.key, _chunk_guard);
+                                    delete_list.push(object_key, _chunk_guard);
                                     Ok(())
                                 },
                             )?;
                         }
                     } else {
                         gc_status.removed_chunks += 1;
-                        gc_status.removed_bytes += content.size;
-                        delete_list.push(content.key, _chunk_guard);
+                        gc_status.removed_bytes += size;
+                        delete_list.push(object_key, _chunk_guard);
                     }
 
                     chunk_count += 1;
@@ -2555,19 +2591,10 @@ impl DataStore {
 
                     // limit pending deletes to avoid holding too many chunk flocks
                     delete_list.conditional_delete_and_drop_locks(s3_client)?;
-                }
-                // Process next batch of chunks if there is more
-                if list_bucket_result.is_truncated {
-                    list_bucket_result =
-                        proxmox_async::runtime::block_on(s3_client.list_objects_v2(
-                            &prefix,
-                            list_bucket_result.next_continuation_token.as_deref(),
-                        ))?;
-                    continue;
-                }
 
-                break;
-            }
+                    Ok(())
+                },
+            )?;
 
             // delete the last batch of objects, if there are any remaining
             delete_list.delete_and_drop_locks(s3_client)?;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper Shan Shaji
@ 2026-09-15 15:59 ` Shan Shaji
  2026-09-25 12:11   ` Christian Ebner
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 3/4] datastore: api: add option to rebuild S3 chunk markers Shan Shaji
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 15:59 UTC (permalink / raw)
  To: pbs-devel

Previously, recreating an S3-backed datastore with the --reuse-datastore
flag fetched only the metadata files into the local cache, without
creating chunk markers. Garbage collection (GC) had to be run to
recreate markers for chunks referenced by the backup indexes.

In order to fix this, create local zero-byte chunk markers by listing
the chunk objects in S3 when reusing a datastore.

Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=7908

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 pbs-datastore/src/datastore.rs | 35 +++++++++++++++++++++++++++++++++-
 src/api2/admin/datastore.rs    | 10 +++++++---
 src/api2/config/datastore.rs   |  6 +++++-
 3 files changed, 46 insertions(+), 5 deletions(-)

diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
index f3eab8a28..d00a0ef1c 100644
--- a/pbs-datastore/src/datastore.rs
+++ b/pbs-datastore/src/datastore.rs
@@ -3171,9 +3171,38 @@ impl DataStore {
         *OLD_LOCKING
     }
 
+    fn create_empty_s3_chunk_markers(
+        &self,
+        worker: &dyn WorkerTaskContext,
+        s3_client: &S3Client,
+    ) -> Result<(), Error> {
+        let _guard = self.inner.chunk_store.mutex().lock().unwrap();
+
+        self.paginate_s3_chunk_objects(worker, s3_client, |object_key, _| -> Result<(), Error> {
+            let (_, digest, _) = match digest_from_object_key(&object_key) {
+                Some(result) => result,
+                None => return Ok(()),
+            };
+
+            unsafe {
+                self.inner
+                    .chunk_store
+                    .replace_chunk_with_marker_or_create_marker(&digest)?;
+            }
+
+            Ok(())
+        })?;
+
+        Ok(())
+    }
+
     /// Fetch contents from S3 object store, clear and replace the local cache store contents.
     /// Returns with error for non-S3 datastore backends.
-    pub async fn s3_refresh(self: &Arc<Self>) -> Result<(), Error> {
+    pub async fn s3_refresh(
+        self: &Arc<Self>,
+        rebuild_chunk_markers: bool,
+        worker: &dyn WorkerTaskContext,
+    ) -> Result<(), Error> {
         match self.backend()? {
             DatastoreBackend::Filesystem => bail!("store '{}' not backed by S3", self.name()),
             DatastoreBackend::S3(s3_client) => {
@@ -3190,6 +3219,10 @@ impl DataStore {
                     let _ = std::fs::remove_dir_all(&tmp_base);
                     return Err(err);
                 }
+
+                if rebuild_chunk_markers {
+                    self.create_empty_s3_chunk_markers(worker, &s3_client)?;
+                }
             }
         }
         Ok(())
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index bc2b2436e..34907fa0a 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -2867,7 +2867,7 @@ pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
         Some(store.clone()),
         auth_id.to_string(),
         to_stdout,
-        move |worker| do_s3_refresh(&store, &worker),
+        move |worker| do_s3_refresh(&store, &worker, false),
     )?;
 
     Ok(json!(upid))
@@ -2875,10 +2875,14 @@ pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
 
 /// Performs an s3 refresh for given datastore. Expects the store to already be in maintenance mode
 /// s3-refresh.
-pub(crate) fn do_s3_refresh(store: &str, worker: &dyn WorkerTaskContext) -> Result<(), Error> {
+pub(crate) fn do_s3_refresh(
+    store: &str,
+    worker: &dyn WorkerTaskContext,
+    rebuild_chunk_markers: bool,
+) -> Result<(), Error> {
     let datastore = DataStore::lookup_datastore(lookup_with(store, Operation::Lookup))?;
     run_maintenance_locked(store, MaintenanceType::S3Refresh, worker, || {
-        proxmox_async::runtime::block_on(datastore.s3_refresh())
+        proxmox_async::runtime::block_on(datastore.s3_refresh(rebuild_chunk_markers, worker))
     })
 }
 
diff --git a/src/api2/config/datastore.rs b/src/api2/config/datastore.rs
index e7028480c..e82e8969a 100644
--- a/src/api2/config/datastore.rs
+++ b/src/api2/config/datastore.rs
@@ -366,7 +366,11 @@ pub fn create_datastore(
             }
 
             if reuse_datastore && backend == DatastoreBackendType::S3 {
-                crate::api2::admin::datastore::do_s3_refresh(&store_name, &worker)?;
+                crate::api2::admin::datastore::do_s3_refresh(
+                    &store_name,
+                    &worker,
+                    reuse_datastore,
+                )?;
             }
             Ok(())
         },
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [RFC PATCH proxmox-backup 3/4] datastore: api: add option to rebuild S3 chunk markers
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper Shan Shaji
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore Shan Shaji
@ 2026-09-15 15:59 ` Shan Shaji
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 4/4] datastore: backup-manager: expose option to rebuild s3 " Shan Shaji
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 15:59 UTC (permalink / raw)
  To: pbs-devel

Add the optional `rebuild-chunk-markers` flag to the datastore
`s3-refresh` API endpoint. When enabled, the refresh lists all chunk
objects in S3 and recreates their local marker files.

Existing local chunk files are replaced with empty marker files. The
option is disabled by default.

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 src/api2/admin/datastore.rs | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 34907fa0a..5df5a6b63 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -2835,6 +2835,13 @@ pub async fn unmount(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<V
             store: {
                 schema: DATASTORE_SCHEMA,
             },
+            "rebuild-chunk-markers": {
+                description: "Create local chunk marker files for all chunks stored in S3. This \
+                requires listing all chunk objects and may take long time for large datastores.",
+                type: bool,
+                optional: true,
+                default: false,
+            },
         }
     },
     returns: {
@@ -2845,7 +2852,11 @@ pub async fn unmount(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<V
     },
 )]
 /// Refresh datastore contents from S3 to local cache store.
-pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
+pub fn s3_refresh(
+    store: String,
+    rebuild_chunk_markers: bool,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
     let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
     let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
 
@@ -2867,7 +2878,7 @@ pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
         Some(store.clone()),
         auth_id.to_string(),
         to_stdout,
-        move |worker| do_s3_refresh(&store, &worker, false),
+        move |worker| do_s3_refresh(&store, &worker, rebuild_chunk_markers),
     )?;
 
     Ok(json!(upid))
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [RFC PATCH proxmox-backup 4/4] datastore: backup-manager: expose option to rebuild s3 chunk markers
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
                   ` (2 preceding siblings ...)
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 3/4] datastore: api: add option to rebuild S3 chunk markers Shan Shaji
@ 2026-09-15 15:59 ` Shan Shaji
  2026-09-15 16:04 ` [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 " Shan Shaji
  2026-09-25 12:11 ` Christian Ebner
  5 siblings, 0 replies; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 15:59 UTC (permalink / raw)
  To: pbs-devel

Add the optional --rebuild-chunk-markers flag to the datastore
s3-refresh command.

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 src/bin/proxmox_backup_manager/datastore.rs | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/bin/proxmox_backup_manager/datastore.rs b/src/bin/proxmox_backup_manager/datastore.rs
index 4c1d9daf5..e595f27be 100644
--- a/src/bin/proxmox_backup_manager/datastore.rs
+++ b/src/bin/proxmox_backup_manager/datastore.rs
@@ -330,6 +330,13 @@ async fn uuid_mount(mut param: Value, _rpcenv: &mut dyn RpcEnvironment) -> Resul
             store: {
                 schema: DATASTORE_SCHEMA,
             },
+            "rebuild-chunk-markers": {
+                description: "Create local chunk marker files for all chunks stored in S3. This \
+                requires listing all chunk objects and may take long time for large datastores.",
+                type: bool,
+                optional: true,
+                default: false,
+            },
         },
     },
 )]
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
                   ` (3 preceding siblings ...)
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 4/4] datastore: backup-manager: expose option to rebuild s3 " Shan Shaji
@ 2026-09-15 16:04 ` Shan Shaji
  2026-09-25 12:11 ` Christian Ebner
  5 siblings, 0 replies; 9+ messages in thread
From: Shan Shaji @ 2026-09-15 16:04 UTC (permalink / raw)
  To: Shan Shaji, pbs-devel

Ahh, added the wrong bugzilla entry in the cover letter subject. It
should be #7908. Sorry for the noise.

- https://bugzilla.proxmox.com/show_bug.cgi?id=7908

On Tue Sep 15, 2026 at 5:59 PM CEST, Shan Shaji wrote:
> Sending this as an RFC to get an initial feedback. I'd really appreciate
> any suggestions or pointers to anything I may have missed.
>
> Reusing an S3-backed datastore currently restores the backup metadata to
> the local cache without creating local chunk markers. A garbage collection
> run is then needed to recreate markers for chunks referenced by the backup
> indexes.
>
> This series addresses #7908 by listing the chunk objects in S3 and creating
> zero-byte local markers when a datastore is recreated with
> --reuse-datastore. It also adds an optional rebuild-chunk-markers parameter
> to the s3-refresh API and exposes it through proxmox-backup-manager:
>
>     proxmox-backup-manager datastore s3-refresh <store> --rebuild-chunk-markers true
>
> The option is disabled by default for explicit refreshes. When enabled,
> it creates markers for all chunk objects with valid chunk keys, including
> chunks not referenced by the restored backup indexes. Existing cached
> chunk files are replaced with empty markers.
>
> Shan Shaji (4):
>   datastore: factor out s3 chunk objects pagination into a helper
>   fix #7908: datastore: create zero-byte markers when re-using datastore
>   datastore: api: add option to rebuild S3 chunk markers
>   datastore: backup-manager: expose option to rebuild s3 chunk markers
>
>  pbs-datastore/src/datastore.rs              | 120 +++++++++++++++-----
>  src/api2/admin/datastore.rs                 |  23 +++-
>  src/api2/config/datastore.rs                |   6 +-
>  src/bin/proxmox_backup_manager/datastore.rs |   7 ++
>  4 files changed, 121 insertions(+), 35 deletions(-)
>
> --
> 2.47.3





^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper Shan Shaji
@ 2026-09-25 12:11   ` Christian Ebner
  0 siblings, 0 replies; 9+ messages in thread
From: Christian Ebner @ 2026-09-25 12:11 UTC (permalink / raw)
  To: Shan Shaji, pbs-devel

Code changes look good to me in general, just a few suggestions to make 
the dependency on the datastore's chunk store instance a bit more explicit.

On 9/15/26 6:00 PM, Shan Shaji wrote:
> This is in preparation to re-use the chunk iteration logic for creating
> empty marker files during s3 refresh.
> 
> Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
> ---
>   pbs-datastore/src/datastore.rs | 85 ++++++++++++++++++++++------------
>   1 file changed, 56 insertions(+), 29 deletions(-)
> 
> diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
> index c3db52269..f3eab8a28 100644
> --- a/pbs-datastore/src/datastore.rs
> +++ b/pbs-datastore/src/datastore.rs
> @@ -2382,6 +2382,47 @@ impl DataStore {
>           }
>       }
>   
> +    /// Fetch chunk objects in batches of max 1000 per request.
> +    /// For each objects listed, the `callback` will be invoked with its
> +    /// S3 object key and size.
> +    fn paginate_s3_chunk_objects<F>(
> +        &self,

comment: As is, this does not need to be a method of DataStore, the 
reference to self is never used after all.

But I would keep it as such and make the dependency on the datastore and 
it's chunk store explicit.

To do this, I suggest to pass in the chunk store by reference to the 
callback and use the reference for all chunk store operations in the 
callback so the relation becomes stringent. So ...

> +        worker: &dyn WorkerTaskContext,
> +        s3_client: &S3Client,
> +        mut callback: F,
> +    ) -> Result<(), Error>
> +    where
> +        F: FnMut(S3ObjectKey, u64) -> Result<(), Error>,

comment: Instead of the generic, this could use an `impl Trait` 
parameter declaration, which makes the method signature cleaner, and 
with the suggested reference to the chunk store passed along would become:

fn paginate_s3_chunk_objects(
      &self,
      worker: &dyn WorkerTaskContext,
      s3_client: &S3Client,
      mut callback: impl FnMut(&ChunkStore, S3ObjectKey, u64) -> 
Result<(), Error>,
) -> Result<(), Error> {

> +    {
> +        let prefix = S3PathPrefix::Some(".chunks/".to_string());
> +        let mut list_bucket_result =
> +            proxmox_async::runtime::block_on(s3_client.list_objects_v2(&prefix, None))
> +                .context("failed to list chunk in s3 object store")?;
> +
> +        loop {
> +            for content in list_bucket_result.contents {
> +                worker.check_abort()?;
> +                worker.fail_on_shutdown()?;
> +

...  here we would now call:
```
callback(self.inner.chunk_store.as_ref(), content.key, content.size)?;
```

> +                callback(content.key, content.size)?;
> +            }
> +
> +            // Process next batch of chunks if there are more.
> +            if list_bucket_result.is_truncated {
> +                list_bucket_result = proxmox_async::runtime::block_on(s3_client.list_objects_v2(
> +                    &prefix,
> +                    list_bucket_result.next_continuation_token.as_deref(),
> +                ))?;
> +
> +                continue;
> +            }
> +
> +            break;
> +        }
> +
> +        Ok(())
> +    }
> +
>       fn garbage_collection_impl(
>           &self,
>           worker: &dyn WorkerTaskContext,
> @@ -2473,28 +2514,23 @@ impl DataStore {
>   
>           if let Some(ref s3_client) = s3_client {
>               let mut chunk_count = 0;
> -            let prefix = S3PathPrefix::Some(".chunks/".to_string());
> -            // Operates in batches of 1000 objects max per request
> -            let mut list_bucket_result =
> -                proxmox_async::runtime::block_on(s3_client.list_objects_v2(&prefix, None))
> -                    .context("failed to list chunk in s3 object store")?;
> -
>               let mut delete_list =
>                   S3DeleteList::with_thresholds(S3_DELETE_BATCH_LIMIT, S3_DELETE_DEFER_LIMIT_SECONDS);
> -            loop {
> -                for content in list_bucket_result.contents {
> -                    worker.check_abort()?;
> -                    worker.fail_on_shutdown()?;
> +
> +            self.paginate_s3_chunk_objects(
> +                worker,
> +                s3_client,
> +                |object_key, size| -> Result<(), Error> {

... this would then have the direct reference to the chunk store:

```
|chunk_store, object_key, size| -> Result<(), Error> {
```

>                       let (chunk_path, digest, bad) =
> -                        match self.chunk_path_from_object_key(&content.key) {
> +                        match self.chunk_path_from_object_key(&object_key) {
>                               Some(path) => path,
> -                            None => continue,
> +                            None => return Ok(()),
>                           };
>   
>                       let timeout = std::time::Duration::from_secs(0);
>                       let _chunk_guard = match self.inner.chunk_store.lock_chunk(&digest, timeout) {

... so operations on that would use that directly instead of binding to 
self, so here for example
```
let _chunk_guard = match chunk_store.lock_chunk(&digest, timeout) {
```

>                           Ok(guard) => guard,
> -                        Err(_) => continue,
> +                        Err(_) => return Ok(()),
>                       };
>                       let _guard = self.inner.chunk_store.mutex().lock().unwrap();
>   
> @@ -2526,7 +2562,7 @@ impl DataStore {
>                                   atime,
>                                   min_atime,
>                                   oldest_writer,
> -                                content.size,
> +                                size,
>                                   bad,
>                                   &mut gc_status,
>                                   || {
> @@ -2537,15 +2573,15 @@ impl DataStore {
>                                               std::fs::remove_file(chunk_path)?;
>                                           }
>                                       }
> -                                    delete_list.push(content.key, _chunk_guard);
> +                                    delete_list.push(object_key, _chunk_guard);
>                                       Ok(())
>                                   },
>                               )?;
>                           }
>                       } else {
>                           gc_status.removed_chunks += 1;
> -                        gc_status.removed_bytes += content.size;
> -                        delete_list.push(content.key, _chunk_guard);
> +                        gc_status.removed_bytes += size;
> +                        delete_list.push(object_key, _chunk_guard);
>                       }
>   
>                       chunk_count += 1;
> @@ -2555,19 +2591,10 @@ impl DataStore {
>   
>                       // limit pending deletes to avoid holding too many chunk flocks
>                       delete_list.conditional_delete_and_drop_locks(s3_client)?;
> -                }
> -                // Process next batch of chunks if there is more
> -                if list_bucket_result.is_truncated {
> -                    list_bucket_result =
> -                        proxmox_async::runtime::block_on(s3_client.list_objects_v2(
> -                            &prefix,
> -                            list_bucket_result.next_continuation_token.as_deref(),
> -                        ))?;
> -                    continue;
> -                }
>   
> -                break;
> -            }
> +                    Ok(())
> +                },
> +            )?;
>   
>               // delete the last batch of objects, if there are any remaining
>               delete_list.delete_and_drop_locks(s3_client)?;





^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore
  2026-09-15 15:59 ` [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore Shan Shaji
@ 2026-09-25 12:11   ` Christian Ebner
  0 siblings, 0 replies; 9+ messages in thread
From: Christian Ebner @ 2026-09-25 12:11 UTC (permalink / raw)
  To: Shan Shaji, pbs-devel

On 9/15/26 6:00 PM, Shan Shaji wrote:
> Previously, recreating an S3-backed datastore with the --reuse-datastore
> flag fetched only the metadata files into the local cache, without
> creating chunk markers. Garbage collection (GC) had to be run to
> recreate markers for chunks referenced by the backup indexes.
> 
> In order to fix this, create local zero-byte chunk markers by listing
> the chunk objects in S3 when reusing a datastore.

This can only work if the whole series depends on [0], so the datastore 
config lock is not held during the whole listing and chunk marker 
creation, which depending on the number of chunks can take a very long time.

The current s3 refresh and other operations which keep the config lock 
for too long were already problematic, but this patch here would greatly 
escalate the problem.

[0] 
https://lore.proxmox.com/pbs-devel/20260803090747.265683-1-c.ebner@proxmox.com/T/#t

> Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=7908
> 
> Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
> ---
>   pbs-datastore/src/datastore.rs | 35 +++++++++++++++++++++++++++++++++-
>   src/api2/admin/datastore.rs    | 10 +++++++---
>   src/api2/config/datastore.rs   |  6 +++++-
>   3 files changed, 46 insertions(+), 5 deletions(-)
> 
> diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
> index f3eab8a28..d00a0ef1c 100644
> --- a/pbs-datastore/src/datastore.rs
> +++ b/pbs-datastore/src/datastore.rs
> @@ -3171,9 +3171,38 @@ impl DataStore {
>           *OLD_LOCKING
>       }
>   
> +    fn create_empty_s3_chunk_markers(
> +        &self,
> +        worker: &dyn WorkerTaskContext,
> +        s3_client: &S3Client,
> +    ) -> Result<(), Error> {
> +        let _guard = self.inner.chunk_store.mutex().lock().unwrap();

Holding the chunk store lock for the whole time here is only acceptable 
since the whole s3-refresh runs protected by it's maintenance mode which 
grants it exclusive access anyways, so that warrants a dedicated 
comment. And the helper should document that requirement as well.

Further, this would greatly benefit from some progress logging, which is 
still lacking for phase 2 of GC on S3 backend as well. Log flooding can 
be avoided by using e.g. the log throttle implementation recently 
introduced by Thomas [0].

[0] 
https://git.proxmox.com/?p=proxmox-backup.git;a=blob;f=pbs-client/src/log_throttle.rs;h=cfaa6c4948a01b7878c04cce08081880891b8a47;hb=709be1a29c2797a320b01be93fa9befcbd51a203

> +
> +        self.paginate_s3_chunk_objects(worker, s3_client, |object_key, _| -> Result<(), Error> {
> +            let (_, digest, _) = match digest_from_object_key(&object_key) {
> +                Some(result) => result,
> +                None => return Ok(()),
> +            };
> +
> +            unsafe {
> +                self.inner
> +                    .chunk_store
> +                    .replace_chunk_with_marker_or_create_marker(&digest)?;

This is not ideal! Any pre-existing, valid locally cached chunk is now 
cleared (and might still be referenced by the in-memory LRU cache).

It would rather make sense to implement a dedicated chunk store helper 
on top of ChunkStore::create_marker_file() which checks for the file to 
pre-exist?

> +            }
> +
> +            Ok(())
> +        })?;
> +
> +        Ok(())
> +    }
> +
>       /// Fetch contents from S3 object store, clear and replace the local cache store contents.
>       /// Returns with error for non-S3 datastore backends.
> -    pub async fn s3_refresh(self: &Arc<Self>) -> Result<(), Error> {
> +    pub async fn s3_refresh(
> +        self: &Arc<Self>,
> +        rebuild_chunk_markers: bool,
> +        worker: &dyn WorkerTaskContext,
> +    ) -> Result<(), Error> {
>           match self.backend()? {
>               DatastoreBackend::Filesystem => bail!("store '{}' not backed by S3", self.name()),
>               DatastoreBackend::S3(s3_client) => {
> @@ -3190,6 +3219,10 @@ impl DataStore {
>                       let _ = std::fs::remove_dir_all(&tmp_base);
>                       return Err(err);
>                   }
> +
> +                if rebuild_chunk_markers {
> +                    self.create_empty_s3_chunk_markers(worker, &s3_client)?;
> +                }
>               }
>           }
>           Ok(())
> diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
> index bc2b2436e..34907fa0a 100644
> --- a/src/api2/admin/datastore.rs
> +++ b/src/api2/admin/datastore.rs
> @@ -2867,7 +2867,7 @@ pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
>           Some(store.clone()),
>           auth_id.to_string(),
>           to_stdout,
> -        move |worker| do_s3_refresh(&store, &worker),
> +        move |worker| do_s3_refresh(&store, &worker, false),
>       )?;
>   
>       Ok(json!(upid))
> @@ -2875,10 +2875,14 @@ pub fn s3_refresh(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Valu
>   
>   /// Performs an s3 refresh for given datastore. Expects the store to already be in maintenance mode
>   /// s3-refresh.
> -pub(crate) fn do_s3_refresh(store: &str, worker: &dyn WorkerTaskContext) -> Result<(), Error> {
> +pub(crate) fn do_s3_refresh(
> +    store: &str,
> +    worker: &dyn WorkerTaskContext,
> +    rebuild_chunk_markers: bool,
> +) -> Result<(), Error> {
>       let datastore = DataStore::lookup_datastore(lookup_with(store, Operation::Lookup))?;
>       run_maintenance_locked(store, MaintenanceType::S3Refresh, worker, || {
> -        proxmox_async::runtime::block_on(datastore.s3_refresh())
> +        proxmox_async::runtime::block_on(datastore.s3_refresh(rebuild_chunk_markers, worker))
>       })
>   }
>   
> diff --git a/src/api2/config/datastore.rs b/src/api2/config/datastore.rs
> index e7028480c..e82e8969a 100644
> --- a/src/api2/config/datastore.rs
> +++ b/src/api2/config/datastore.rs
> @@ -366,7 +366,11 @@ pub fn create_datastore(
>               }
>   
>               if reuse_datastore && backend == DatastoreBackendType::S3 {
> -                crate::api2::admin::datastore::do_s3_refresh(&store_name, &worker)?;
> +                crate::api2::admin::datastore::do_s3_refresh(
> +                    &store_name,
> +                    &worker,
> +                    reuse_datastore,
> +                )?;
>               }
>               Ok(())
>           },





^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers
  2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
                   ` (4 preceding siblings ...)
  2026-09-15 16:04 ` [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 " Shan Shaji
@ 2026-09-25 12:11 ` Christian Ebner
  5 siblings, 0 replies; 9+ messages in thread
From: Christian Ebner @ 2026-09-25 12:11 UTC (permalink / raw)
  To: Shan Shaji, pbs-devel

Thanks for the patches, left a few suggestions and major concerns on 
individual patches. It is especially crucial to take the possible 
runtime of this into account!

What is currently complete missing is any form of exposing this optional 
behavior to the user in the UI. It might make sense to have such a flag 
in the datastore create window, which is the main use-case for this. I 
see less use for normal s3 refresh operations.

On 9/15/26 6:00 PM, Shan Shaji wrote:
> Sending this as an RFC to get an initial feedback. I'd really appreciate
> any suggestions or pointers to anything I may have missed.
> 
> Reusing an S3-backed datastore currently restores the backup metadata to
> the local cache without creating local chunk markers. A garbage collection
> run is then needed to recreate markers for chunks referenced by the backup
> indexes.
> 
> This series addresses #7908 by listing the chunk objects in S3 and creating
> zero-byte local markers when a datastore is recreated with
> --reuse-datastore. It also adds an optional rebuild-chunk-markers parameter
> to the s3-refresh API and exposes it through proxmox-backup-manager:
> 
>      proxmox-backup-manager datastore s3-refresh <store> --rebuild-chunk-markers true
> 
> The option is disabled by default for explicit refreshes. When enabled,
> it creates markers for all chunk objects with valid chunk keys, including
> chunks not referenced by the restored backup indexes. Existing cached
> chunk files are replaced with empty markers.
> 
> Shan Shaji (4):
>    datastore: factor out s3 chunk objects pagination into a helper
>    fix #7908: datastore: create zero-byte markers when re-using datastore
>    datastore: api: add option to rebuild S3 chunk markers
>    datastore: backup-manager: expose option to rebuild s3 chunk markers
> 
>   pbs-datastore/src/datastore.rs              | 120 +++++++++++++++-----
>   src/api2/admin/datastore.rs                 |  23 +++-
>   src/api2/config/datastore.rs                |   6 +-
>   src/bin/proxmox_backup_manager/datastore.rs |   7 ++
>   4 files changed, 121 insertions(+), 35 deletions(-)
> 
> --
> 2.47.3
> 
> 
> 
> 





^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-25 12:12 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-15 15:59 [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 chunk markers Shan Shaji
2026-09-15 15:59 ` [RFC PATCH proxmox-backup 1/4] datastore: factor out s3 chunk objects pagination into a helper Shan Shaji
2026-09-25 12:11   ` Christian Ebner
2026-09-15 15:59 ` [RFC PATCH proxmox-backup 2/4] fix #7908: datastore: create zero-byte markers when re-using datastore Shan Shaji
2026-09-25 12:11   ` Christian Ebner
2026-09-15 15:59 ` [RFC PATCH proxmox-backup 3/4] datastore: api: add option to rebuild S3 chunk markers Shan Shaji
2026-09-15 15:59 ` [RFC PATCH proxmox-backup 4/4] datastore: backup-manager: expose option to rebuild s3 " Shan Shaji
2026-09-15 16:04 ` [RFC PATCH proxmox-backup 0/4] fix #7903: rebuild local S3 " Shan Shaji
2026-09-25 12:11 ` Christian Ebner

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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal