mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-04 12:27:43 +00:00
fix(utils): tolerate bavail greater than bfree on Linux (#3119)
* fix(utils): tolerate bavail greater than bfree on Linux Treat f_bavail > f_bfree as a compatibility edge case instead of fatal corruption during startup disk info probing. - keep corruption checks for other invalid counter relationships - log a warning and clamp reserved blocks to 0 when bavail exceeds bfree - add linux unit coverage for the compatibility and overflow paths Refs #3025 * fix(utils): cap bavail to bfree for Linux statfs edge * fix(utils): rate-limit Linux statfs compatibility warning * test(utils): cover Linux statfs capacity math Expand Linux statfs capacity math tests around normal, equal, zero, and fully-free block counter combinations. Refs #3025 * fix(utils): avoid statfs warning guard reallocations * test(utils): align statfs reserved block expectations * fmt * fix(utils): avoid warning guard path allocation Check the warn-once set before allocating a PathBuf so repeated bavail/bfree compatibility warnings stay low-overhead. Refs #3025
This commit is contained in:
+136
-29
@@ -19,10 +19,13 @@ use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, Error, ErrorKind, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use tracing::warn;
|
||||
|
||||
static BAVAIL_GT_BFREE_WARNING_PATHS: OnceLock<Mutex<BTreeSet<PathBuf>>> = OnceLock::new();
|
||||
|
||||
/// Returns total and free bytes available in a directory, e.g. `/`.
|
||||
pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
|
||||
let path_display = p.as_ref().display();
|
||||
// Use statfs on Linux to get access to f_type (filesystem magic number)
|
||||
let stat = statfs(p.as_ref())?;
|
||||
|
||||
@@ -42,34 +45,7 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
|
||||
let bfree = stat.f_bfree as u64;
|
||||
let bavail = stat.f_bavail as u64;
|
||||
let blocks = stat.f_blocks as u64;
|
||||
|
||||
let reserved = match bfree.checked_sub(bavail) {
|
||||
Some(reserved) => reserved,
|
||||
None => {
|
||||
return Err(Error::other(format!(
|
||||
"detected f_bavail space ({bavail}) > f_bfree space ({bfree}), fs corruption at ({path_display}). please run 'fsck'",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let total = match blocks.checked_sub(reserved) {
|
||||
Some(total) => total * bsize,
|
||||
None => {
|
||||
return Err(Error::other(format!(
|
||||
"detected reserved space ({reserved}) > blocks space ({blocks}), fs corruption at ({path_display}). please run 'fsck'",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let free = bavail * bsize;
|
||||
let used = match total.checked_sub(free) {
|
||||
Some(used) => used,
|
||||
None => {
|
||||
return Err(Error::other(format!(
|
||||
"detected free space ({free}) > total drive space ({total}), fs corruption at ({path_display}). please run 'fsck'"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let (total, free, used) = calculate_space_usage(blocks, bfree, bavail, bsize, p.as_ref())?;
|
||||
|
||||
let st = rustix::fs::stat(p.as_ref())?;
|
||||
|
||||
@@ -86,6 +62,59 @@ pub fn get_info(p: impl AsRef<Path>) -> std::io::Result<DiskInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_space_usage(blocks: u64, bfree: u64, bavail: u64, bsize: u64, path: &Path) -> std::io::Result<(u64, u64, u64)> {
|
||||
let available = if bfree < bavail {
|
||||
if should_warn_bavail_greater_than_bfree(path) {
|
||||
warn!(
|
||||
path = %path.display(),
|
||||
f_bfree = bfree,
|
||||
f_bavail = bavail,
|
||||
"detected f_bavail greater than f_bfree, capping available blocks to f_bfree for compatibility"
|
||||
);
|
||||
}
|
||||
bfree
|
||||
} else {
|
||||
bavail
|
||||
};
|
||||
let reserved = bfree - available;
|
||||
|
||||
let total = match blocks.checked_sub(reserved) {
|
||||
Some(total) => total * bsize,
|
||||
None => {
|
||||
return Err(Error::other(format!(
|
||||
"detected reserved space ({reserved}) > blocks space ({blocks}), fs corruption at ({}). please run 'fsck'",
|
||||
path.display(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let free = available * bsize;
|
||||
let used = match total.checked_sub(free) {
|
||||
Some(used) => used,
|
||||
None => {
|
||||
return Err(Error::other(format!(
|
||||
"detected free space ({free}) > total drive space ({total}), fs corruption at ({}). please run 'fsck'",
|
||||
path.display(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok((total, free, used))
|
||||
}
|
||||
|
||||
fn should_warn_bavail_greater_than_bfree(path: &Path) -> bool {
|
||||
let warned_paths = BAVAIL_GT_BFREE_WARNING_PATHS.get_or_init(|| Mutex::new(BTreeSet::new()));
|
||||
let mut warned_paths = match warned_paths.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
if warned_paths.contains(path) {
|
||||
false
|
||||
} else {
|
||||
warned_paths.insert(path.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn same_disk(disk1: &str, disk2: &str) -> std::io::Result<bool> {
|
||||
let stat1 = rustix::fs::stat(disk1)?;
|
||||
let stat2 = rustix::fs::stat(disk2)?;
|
||||
@@ -407,4 +436,82 @@ mod tests {
|
||||
let ids = resolve_block_device_ids(major, minor).unwrap();
|
||||
assert_eq!(ids.into_iter().collect::<Vec<_>>(), vec![format!("{major}:{minor}")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_normal_bfree_greater_than_bavail() {
|
||||
// Typical ext4/xfs scenario: bavail < bfree due to reserved blocks
|
||||
let blocks = 1_000_u64;
|
||||
let bfree = 900_u64;
|
||||
let bavail = 850_u64;
|
||||
let bsize = 4_096_u64;
|
||||
|
||||
let (total, free, used) = calculate_space_usage(blocks, bfree, bavail, bsize, Path::new("/data")).unwrap();
|
||||
let reserved = bfree - bavail;
|
||||
assert_eq!(total, (blocks - reserved) * bsize);
|
||||
assert_eq!(free, bavail * bsize);
|
||||
assert_eq!(used, total - free);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_bfree_equals_bavail() {
|
||||
// No reserved blocks: bfree == bavail (e.g. FAT/exFAT or root user)
|
||||
let blocks = 500_u64;
|
||||
let bfree = 400_u64;
|
||||
let bavail = 400_u64;
|
||||
let bsize = 4_096_u64;
|
||||
|
||||
let (total, free, used) = calculate_space_usage(blocks, bfree, bavail, bsize, Path::new("/data")).unwrap();
|
||||
assert_eq!(total, blocks * bsize);
|
||||
assert_eq!(free, bfree * bsize);
|
||||
assert_eq!(used, (blocks - bfree) * bsize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_all_zero_blocks() {
|
||||
let (total, free, used) = calculate_space_usage(0, 0, 0, 4_096, Path::new("/data")).unwrap();
|
||||
assert_eq!(total, 0);
|
||||
assert_eq!(free, 0);
|
||||
assert_eq!(used, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_all_blocks_free() {
|
||||
let blocks = 1_000_u64;
|
||||
let bfree = 1_000_u64;
|
||||
let bavail = 1_000_u64;
|
||||
let bsize = 4_096_u64;
|
||||
|
||||
let (total, free, used) = calculate_space_usage(blocks, bfree, bavail, bsize, Path::new("/data")).unwrap();
|
||||
assert_eq!(total, blocks * bsize);
|
||||
assert_eq!(free, blocks * bsize);
|
||||
assert_eq!(used, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_allows_bavail_greater_than_bfree() {
|
||||
let blocks = 1_000_u64;
|
||||
let bfree = 900_u64;
|
||||
let bavail = 920_u64;
|
||||
let bsize = 4_096_u64;
|
||||
|
||||
let (total, free, used) = calculate_space_usage(blocks, bfree, bavail, bsize, Path::new("/data")).unwrap();
|
||||
assert_eq!(total, blocks * bsize);
|
||||
assert_eq!(free, bfree * bsize);
|
||||
assert_eq!(used, total - free);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_space_usage_rejects_free_greater_than_total() {
|
||||
let err = calculate_space_usage(100, 120, 120, 4_096, Path::new("/data")).unwrap_err();
|
||||
assert!(err.to_string().contains("detected free space"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bavail_greater_than_bfree_warning_is_once_per_path() {
|
||||
let path = Path::new("/data/rustfs-bavail-warning-once");
|
||||
|
||||
assert!(should_warn_bavail_greater_than_bfree(path));
|
||||
assert!(!should_warn_bavail_greater_than_bfree(path));
|
||||
assert!(should_warn_bavail_greater_than_bfree(Path::new("/data/rustfs-bavail-warning-other")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user