From: Robert Obkircher <r.obkircher@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup 06/10] chunk_store: add method to limit file system usage
Date: Thu, 30 Apr 2026 17:05:47 +0200 [thread overview]
Message-ID: <20260430150607.330413-10-r.obkircher@proxmox.com> (raw)
In-Reply-To: <20260430150607.330413-1-r.obkircher@proxmox.com>
Provide a way to check whether enough space is available to write new
backup data to a local chunk store. This is especially important on
copy-on-write file systems where GC and prune jobs need additional
space for metadata updates.
The check is not completely safe because multiple threads/processes
could perform it at the same time, but with a big enough reservation
it should be good enough in practice.
Caching is used to avoid unnecessary syscalls, but this is likely only
beneficial on NFS.
Signed-off-by: Robert Obkircher <r.obkircher@proxmox.com>
---
pbs-datastore/src/chunk_store.rs | 12 ++++
pbs-datastore/src/file_system_limit.rs | 87 ++++++++++++++++++++++++++
pbs-datastore/src/lib.rs | 2 +
3 files changed, 101 insertions(+)
create mode 100644 pbs-datastore/src/file_system_limit.rs
diff --git a/pbs-datastore/src/chunk_store.rs b/pbs-datastore/src/chunk_store.rs
index 68db88eab..a02f437c1 100644
--- a/pbs-datastore/src/chunk_store.rs
+++ b/pbs-datastore/src/chunk_store.rs
@@ -23,6 +23,7 @@ use crate::data_blob::DataChunkBuilder;
use crate::file_formats::{
COMPRESSED_BLOB_MAGIC_1_0, ENCRYPTED_BLOB_MAGIC_1_0, UNCOMPRESSED_BLOB_MAGIC_1_0,
};
+use crate::file_system_limit::FileSystemLimit;
use crate::{DataBlob, LocalDatastoreLruCache};
const USING_MARKER_FILENAME_EXT: &str = "using";
@@ -35,6 +36,7 @@ pub struct ChunkStore {
mutex: Mutex<()>,
locker: Option<Arc<Mutex<ProcessLocker>>>,
sync_level: DatastoreFSyncLevel,
+ fs_limit: FileSystemLimit,
}
// TODO: what about sysctl setting vm.vfs_cache_pressure (0 - 100) ?
@@ -82,6 +84,7 @@ impl ChunkStore {
mutex: Mutex::new(()),
locker: None,
sync_level: Default::default(),
+ fs_limit: FileSystemLimit::new(None),
}
}
@@ -206,6 +209,7 @@ impl ChunkStore {
locker: Some(locker),
mutex: Mutex::new(()),
sync_level,
+ fs_limit: FileSystemLimit::new(None),
})
}
@@ -966,6 +970,10 @@ impl ChunkStore {
}
(chunk_path, counter)
}
+
+ pub(crate) fn check_space(&self, size: u64) -> Result<(), Error> {
+ self.fs_limit.check_available(&self.base, size)
+ }
}
#[derive(PartialEq)]
@@ -1001,6 +1009,10 @@ fn test_chunk_store1() {
.build()
.unwrap();
+ chunk_store
+ .check_space(chunk.raw_size())
+ .expect("enough space");
+
let (exists, _) = chunk_store.insert_chunk(&chunk, &digest).unwrap();
assert!(!exists);
diff --git a/pbs-datastore/src/file_system_limit.rs b/pbs-datastore/src/file_system_limit.rs
new file mode 100644
index 000000000..fab62d046
--- /dev/null
+++ b/pbs-datastore/src/file_system_limit.rs
@@ -0,0 +1,87 @@
+use std::path::Path;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{Duration, Instant};
+
+use anyhow::{bail, format_err, Error};
+
+/// Cached file system space availability check.
+///
+/// Supports reserving a safety buffer because multiple threads
+/// and processes may pass the check at the same time before they
+/// write.
+pub struct FileSystemLimit {
+ reserved: AtomicU64,
+ base: Instant,
+ elapsed_nanos: AtomicU64,
+ available: AtomicU64,
+}
+
+/// Encode `None` as `MAX` because nobody has enough storage to
+/// notice the difference.
+fn encode_reserved(bytes: Option<u64>) -> u64 {
+ bytes.map_or(u64::MAX, |b| b.min(u64::MAX - 1))
+}
+
+impl FileSystemLimit {
+ /// Specify the amount of reserved space for checks, or disable them with `None`.
+ pub fn new(reserved_space: Option<u64>) -> Self {
+ Self {
+ reserved: AtomicU64::new(encode_reserved(reserved_space)),
+ base: Instant::now(),
+ elapsed_nanos: AtomicU64::new(0),
+ available: AtomicU64::new(0),
+ }
+ }
+
+ /// Specify the amount of reserved space for checks, or disable them with `None`.
+ pub fn set_reserved_space(&self, bytes: Option<u64>) {
+ self.reserved
+ .store(encode_reserved(bytes), Ordering::Release);
+ }
+
+ /// Check if there is probably enough space to write `size` bytes.
+ ///
+ /// Repeated calls must specify paths to the same file system.
+ pub fn check_available(&self, path: &Path, size: u64) -> Result<(), Error> {
+ let reserved = self.reserved.load(Ordering::Acquire);
+ if reserved == u64::MAX {
+ return Ok(()); // disabled
+ }
+ let required = reserved.saturating_add(size);
+
+ let since_base = self.base.elapsed().as_nanos() as u64;
+ let last_update = self.elapsed_nanos.load(Ordering::Acquire);
+ let since_update = since_base.saturating_sub(last_update);
+
+ // Limit max age in case of unexpected changes like a manual resize of the file system.
+ if last_update != 0 && since_update as u128 <= Duration::from_secs(1).as_nanos() {
+ // Assume at most 100 GB/s (1 GB/s = 1 B/ns)
+ let max_written = 100 * since_update;
+
+ let available = self.available.load(Ordering::Acquire);
+ if required.saturating_add(max_written) <= available {
+ log::trace!( "file_system_limit: cached, path={path:?}, available={available}, requested={size}, reserved={reserved}");
+ return Ok(());
+ }
+ }
+
+ // Repeated calls on a local file system take less than 2 microseconds,
+ // so it should be fine if multiple threads get here at the same time
+ // and race on the stores below.
+ let info = proxmox_sys::fs::fs_info(path)
+ .map_err(|e| format_err!("failed to read file system info for {path:?} - {e}"))?;
+
+ let available = info.available;
+ self.available.store(available, Ordering::Release);
+ self.elapsed_nanos.store(since_base, Ordering::Release);
+
+ log::trace!( "file_system_limit: uncached, path={path:?}, available={available}, requested={size}, reserved={reserved}");
+ if required > available {
+ // The UI also shows this instead of `info.total`
+ let total = info.used + info.available;
+
+ bail!("Not enough space: path={path:?}, available={available}/{total}, requested={size}, reserved={reserved}");
+ }
+ Ok(())
+ }
+}
diff --git a/pbs-datastore/src/lib.rs b/pbs-datastore/src/lib.rs
index 6647ee2b6..6a0c58a91 100644
--- a/pbs-datastore/src/lib.rs
+++ b/pbs-datastore/src/lib.rs
@@ -222,6 +222,8 @@ pub use datastore::{
S3_CLIENT_REQUEST_COUNTER_BASE_PATH, S3_DATASTORE_IN_USE_MARKER,
};
+mod file_system_limit;
+
mod hierarchy;
pub use hierarchy::{
ListGroups, ListGroupsType, ListNamespaces, ListNamespacesRecursive, ListSnapshots,
--
2.47.3
next prev parent reply other threads:[~2026-04-30 15:06 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-30 15:05 [RFC proxmox{,-backup} 00/13] gc maintenance mode and full datastore protection Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox 1/3] pbs-api-types: add datastore operation variant for reclaiming storage Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox 2/3] pbs-abi-types: add GarbageCollection maintenance mode Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox 3/3] pbs-api-types: add reserved space to datastore tuning options Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 01/10] task tracking: count Reclaim datastore operations as writes Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 02/10] datastore: open datastores with Reclaim instead of Write operation Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 03/10] fix #5797: www: display new GarbageCollection maintenance mode Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 04/10] www: access active operation fields by name instead of index Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 05/10] www: don't claim that all active writers are gc mode conflicts Robert Obkircher
2026-04-30 15:05 ` Robert Obkircher [this message]
2026-04-30 15:05 ` [PATCH proxmox-backup 07/10] chunk_store: check file system space before inserting new chunks Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 08/10] datastore: check file system space for blobs and group notes Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 09/10] api2: backup: check space for fixed and dynamic index files Robert Obkircher
2026-04-30 15:05 ` [PATCH proxmox-backup 10/10] fix #7254: datastore: refuse new backps when capacity is almost full Robert Obkircher
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=20260430150607.330413-10-r.obkircher@proxmox.com \
--to=r.obkircher@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox