Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc2996e536 | ||
|
|
ef02b22b72 | ||
|
|
cc26052cfe |
@@ -655,6 +655,17 @@ fn decode_nibble(b: u8) -> Option<u8> {
|
||||
|
||||
/// statvfs on the given path. Uses libc directly — cheap enough
|
||||
/// that we don't need to cache. Silent on error (returns None).
|
||||
///
|
||||
/// Bug fix 2026-08-02: `used` was computed as `total - f_bavail`.
|
||||
/// `f_bavail` is space available to an *unprivileged* user, which
|
||||
/// excludes ext4's reserved-blocks-for-root margin (~5% of the
|
||||
/// filesystem by default) — so that formula silently folded the
|
||||
/// entire reserved margin into "used", overstating usage by exactly
|
||||
/// that amount on every node (worse on bigger disks: ~21GB on a
|
||||
/// 466GB root, proportionally more on multi-TB ones). `df`'s Used
|
||||
/// column is `total - f_bfree` (raw free blocks, root-reserved or
|
||||
/// not) — matching that here is what makes the dashboard agree with
|
||||
/// `df` instead of silently running high.
|
||||
fn filesystem_usage(path: &std::path::Path) -> Option<FilesystemUsage> {
|
||||
let cpath = std::ffi::CString::new(path.as_os_str().to_str()?).ok()?;
|
||||
// SAFETY: statvfs writes to a zero-initialised struct; we
|
||||
@@ -667,7 +678,8 @@ fn filesystem_usage(path: &std::path::Path) -> Option<FilesystemUsage> {
|
||||
let bsize = stat.f_frsize as u64;
|
||||
let total = stat.f_blocks as u64 * bsize;
|
||||
let avail = stat.f_bavail as u64 * bsize;
|
||||
let used = total.saturating_sub(avail);
|
||||
let free = stat.f_bfree as u64 * bsize;
|
||||
let used = total.saturating_sub(free);
|
||||
Some(FilesystemUsage {
|
||||
mount_point: path.display().to_string(),
|
||||
total_bytes: total,
|
||||
@@ -676,6 +688,34 @@ fn filesystem_usage(path: &std::path::Path) -> Option<FilesystemUsage> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod filesystem_usage_tests {
|
||||
use super::filesystem_usage;
|
||||
|
||||
#[test]
|
||||
fn used_plus_available_never_exceeds_total() {
|
||||
// `available` is a subset of the space `used` now excludes
|
||||
// (used = total - free, and available = f_bavail <= f_bfree),
|
||||
// so this invariant holds regardless of any root-reserved
|
||||
// margin -- the regression this guards against is `used`
|
||||
// being computed as `total - available`, which collapses
|
||||
// that margin into `used` and can make `used + available`
|
||||
// overshoot `total` in the other direction (it wouldn't here,
|
||||
// but the two numbers would silently disagree with `df`).
|
||||
let fs = filesystem_usage(std::path::Path::new(".")).expect("statvfs on cwd");
|
||||
assert!(fs.total_bytes > 0);
|
||||
assert!(fs.used_bytes <= fs.total_bytes);
|
||||
assert!(fs.available_bytes <= fs.total_bytes.saturating_sub(fs.used_bytes) + 4096,
|
||||
"available ({}) should fit within total-used ({}) modulo one block; got used={}, total={}",
|
||||
fs.available_bytes, fs.total_bytes.saturating_sub(fs.used_bytes), fs.used_bytes, fs.total_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_path_returns_none() {
|
||||
assert!(filesystem_usage(std::path::Path::new("/this/path/does/not/exist/at/all")).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// Query systemd for a user-scope timer's next-fire + last result.
|
||||
/// Shells out to systemctl. Silent on any failure — dashboards
|
||||
/// should degrade to "unknown" rather than 500.
|
||||
|
||||
Reference in New Issue
Block a user