fix(scanner): reduce non-actionable scan noise (#3840)

This commit is contained in:
cxymds
2026-06-25 14:35:10 +08:00
committed by GitHub
parent bfb2a95e3d
commit 4cb28abfc9
3 changed files with 167 additions and 4 deletions
+1 -1
View File
@@ -49,4 +49,4 @@ criterion = { workspace = true }
serial_test = { workspace = true }
temp-env = { workspace = true, features = ["async_closure"] }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["test-util"] }
tokio = { workspace = true, features = ["test-util", "macros", "rt"] }
+101 -1
View File
@@ -25,7 +25,8 @@ use rustfs_io_metrics::capacity_metrics::{
record_capacity_stall_detected, record_capacity_symlink, record_capacity_timeout_fallback,
};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
use std::time::{Duration, Instant};
use tracing::{debug, info, warn};
use walkdir::WalkDir;
@@ -48,6 +49,9 @@ const EVENT_CAPACITY_SCAN_TRAVERSAL_FAILED: &str = "capacity_scan_traversal_fail
const EVENT_CAPACITY_SCAN_METADATA_FAILED: &str = "capacity_scan_metadata_failed";
const EVENT_CAPACITY_SCAN_SAMPLING_APPLIED: &str = "capacity_scan_sampling_applied";
const EVENT_CAPACITY_SCAN_EXACT_COMPLETED: &str = "capacity_scan_exact_completed";
const RUSTFS_META_BUCKET: &str = ".rustfs.sys";
const RUSTFS_META_TMP_DIR: &str = "tmp";
const RUSTFS_META_TMP_TRASH_DIR: &str = ".trash";
#[derive(Debug)]
struct DiskScanOutcome {
@@ -113,6 +117,27 @@ fn disk_scope_key(disk: &CapacityDiskRef) -> CapacityScopeDisk {
}
}
fn normal_component_eq(component: Option<Component<'_>>, expected: &str) -> bool {
matches!(component, Some(Component::Normal(value)) if value == OsStr::new(expected))
}
fn is_tmp_trash_metadata_not_found(scan_root: &Path, entry_path: &Path, error_kind: Option<std::io::ErrorKind>) -> bool {
if error_kind != Some(std::io::ErrorKind::NotFound) {
return false;
}
let Ok(relative_path) = entry_path.strip_prefix(scan_root) else {
return false;
};
let mut components = relative_path.components();
normal_component_eq(components.next(), RUSTFS_META_BUCKET)
&& normal_component_eq(components.next(), RUSTFS_META_TMP_DIR)
&& normal_component_eq(components.next(), RUSTFS_META_TMP_TRASH_DIR)
&& matches!(components.next(), Some(Component::Normal(_)))
&& components.all(|component| matches!(component, Component::Normal(_)))
}
async fn scan_disk_used_capacity(disk: CapacityDiskRef) -> DiskScanOutcome {
let disk_label = disk_metric_label(&disk);
let drive_path = disk.drive_path.clone();
@@ -585,6 +610,20 @@ async fn get_dir_size_async(path: &Path) -> Result<CapacityScanResult, std::io::
let metadata = match entry.metadata() {
Ok(meta) => meta,
Err(err) => {
if is_tmp_trash_metadata_not_found(&path, entry.path(), err.io_error().map(|err| err.kind())) {
debug!(
event = EVENT_CAPACITY_SCAN_METADATA_FAILED,
component = LOG_COMPONENT_CAPACITY,
subsystem = LOG_SUBSYSTEM_SCAN,
result = "ignored",
reason = "tmp_trash_not_found",
entry_path = ?entry.path(),
file_count,
"capacity scan ignored tmp trash metadata race"
);
continue;
}
warn!(
event = EVENT_CAPACITY_SCAN_METADATA_FAILED,
component = LOG_COMPONENT_CAPACITY,
@@ -902,6 +941,67 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn test_tmp_trash_metadata_not_found_predicate_accepts_trash_not_found() {
let scan_root = Path::new("/disk");
let entry_path = scan_root
.join(RUSTFS_META_BUCKET)
.join(RUSTFS_META_TMP_DIR)
.join(RUSTFS_META_TMP_TRASH_DIR)
.join("cleanup-id")
.join("part.1");
assert!(is_tmp_trash_metadata_not_found(
scan_root,
&entry_path,
Some(std::io::ErrorKind::NotFound)
));
}
#[test]
fn test_tmp_trash_metadata_not_found_predicate_rejects_non_trash_cases() {
let scan_root = Path::new("/disk");
let ordinary_object = scan_root.join("bucket").join("object").join("part.1");
let tmp_non_trash = scan_root
.join(RUSTFS_META_BUCKET)
.join(RUSTFS_META_TMP_DIR)
.join("upload-id")
.join("part.1");
let outside_scan_root = Path::new("/outside")
.join(RUSTFS_META_BUCKET)
.join(RUSTFS_META_TMP_DIR)
.join(RUSTFS_META_TMP_TRASH_DIR)
.join("cleanup-id")
.join("part.1");
let trash_permission_denied = scan_root
.join(RUSTFS_META_BUCKET)
.join(RUSTFS_META_TMP_DIR)
.join(RUSTFS_META_TMP_TRASH_DIR)
.join("cleanup-id")
.join("part.1");
assert!(!is_tmp_trash_metadata_not_found(
scan_root,
&ordinary_object,
Some(std::io::ErrorKind::NotFound)
));
assert!(!is_tmp_trash_metadata_not_found(
scan_root,
&tmp_non_trash,
Some(std::io::ErrorKind::NotFound)
));
assert!(!is_tmp_trash_metadata_not_found(
scan_root,
&outside_scan_root,
Some(std::io::ErrorKind::NotFound)
));
assert!(!is_tmp_trash_metadata_not_found(
scan_root,
&trash_permission_denied,
Some(std::io::ErrorKind::PermissionDenied)
));
}
#[tokio::test]
async fn test_calculate_data_dir_used_capacity_returns_partial_success() {
use std::fs::File;
+65 -2
View File
@@ -463,14 +463,14 @@ fn build_object_heal_request(
fn resolve_object_heal_entry(entries: &MetaCacheEntries, resolver: MetadataResolutionParams) -> Option<MetaCacheEntry> {
if let Some(entry) = entries.resolve(resolver) {
return Some(entry);
return entry.is_object().then_some(entry);
}
entries
.as_ref()
.iter()
.flatten()
.find(|entry| !entry.name.ends_with(SLASH_SEPARATOR))
.find(|entry| entry.is_object() && !entry.name.ends_with(SLASH_SEPARATOR))
.cloned()
}
@@ -3120,6 +3120,48 @@ mod tests {
);
}
#[test]
fn test_resolve_object_heal_entry_skips_resolved_empty_directory_candidate() {
let entries = MetaCacheEntries(vec![
Some(MetaCacheEntry {
name: "object/".to_string(),
metadata: Vec::new(),
..Default::default()
}),
Some(MetaCacheEntry {
name: "object/".to_string(),
metadata: Vec::new(),
..Default::default()
}),
]);
assert!(
resolve_object_heal_entry(&entries, test_metadata_resolver("bucket")).is_none(),
"resolved empty directory candidates must not be submitted as object heals"
);
}
#[test]
fn test_resolve_object_heal_entry_skips_only_empty_directory_fallback_candidates() {
let entries = MetaCacheEntries(vec![
Some(MetaCacheEntry {
name: "object/".to_string(),
metadata: Vec::new(),
..Default::default()
}),
Some(MetaCacheEntry {
name: "prefix/".to_string(),
metadata: Vec::new(),
..Default::default()
}),
]);
assert!(
resolve_object_heal_entry(&entries, test_metadata_resolver("bucket")).is_none(),
"unresolved fallback must ignore empty directory candidates"
);
}
#[test]
fn test_resolve_object_heal_entry_uses_plain_fallback_after_trailing_slash() {
let entries = MetaCacheEntries(vec![
@@ -3141,6 +3183,27 @@ mod tests {
assert_eq!(entry.name, "object");
}
#[test]
fn test_resolve_object_heal_entry_uses_plain_fallback_after_empty_directory_candidate() {
let entries = MetaCacheEntries(vec![
Some(MetaCacheEntry {
name: "object/".to_string(),
metadata: Vec::new(),
..Default::default()
}),
Some(MetaCacheEntry {
name: "object".to_string(),
metadata: vec![1, 2, 3],
..Default::default()
}),
]);
let entry = resolve_object_heal_entry(&entries, test_metadata_resolver("bucket"))
.expect("plain object fallback should remain eligible after an empty directory candidate");
assert_eq!(entry.name, "object");
}
#[test]
fn test_resolve_object_heal_entry_preserves_resolved_trailing_slash_object() {
let metadata = metadata_for_object("bucket", "object/");