Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef02b22b72 |
@@ -655,6 +655,17 @@ fn decode_nibble(b: u8) -> Option<u8> {
|
|||||||
|
|
||||||
/// statvfs on the given path. Uses libc directly — cheap enough
|
/// statvfs on the given path. Uses libc directly — cheap enough
|
||||||
/// that we don't need to cache. Silent on error (returns None).
|
/// 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> {
|
fn filesystem_usage(path: &std::path::Path) -> Option<FilesystemUsage> {
|
||||||
let cpath = std::ffi::CString::new(path.as_os_str().to_str()?).ok()?;
|
let cpath = std::ffi::CString::new(path.as_os_str().to_str()?).ok()?;
|
||||||
// SAFETY: statvfs writes to a zero-initialised struct; we
|
// 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 bsize = stat.f_frsize as u64;
|
||||||
let total = stat.f_blocks as u64 * bsize;
|
let total = stat.f_blocks as u64 * bsize;
|
||||||
let avail = stat.f_bavail 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 {
|
Some(FilesystemUsage {
|
||||||
mount_point: path.display().to_string(),
|
mount_point: path.display().to_string(),
|
||||||
total_bytes: total,
|
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.
|
/// Query systemd for a user-scope timer's next-fire + last result.
|
||||||
/// Shells out to systemctl. Silent on any failure — dashboards
|
/// Shells out to systemctl. Silent on any failure — dashboards
|
||||||
/// should degrade to "unknown" rather than 500.
|
/// should degrade to "unknown" rather than 500.
|
||||||
|
|||||||
Reference in New Issue
Block a user