From 4cb28abfc9e4db21e10c771c4f5be236798a2d97 Mon Sep 17 00:00:00 2001 From: cxymds Date: Thu, 25 Jun 2026 14:35:10 +0800 Subject: [PATCH] fix(scanner): reduce non-actionable scan noise (#3840) --- crates/object-capacity/Cargo.toml | 2 +- crates/object-capacity/src/scan.rs | 102 ++++++++++++++++++++++++++- crates/scanner/src/scanner_folder.rs | 67 +++++++++++++++++- 3 files changed, 167 insertions(+), 4 deletions(-) diff --git a/crates/object-capacity/Cargo.toml b/crates/object-capacity/Cargo.toml index 04413ec34..b48d4cadf 100644 --- a/crates/object-capacity/Cargo.toml +++ b/crates/object-capacity/Cargo.toml @@ -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"] } diff --git a/crates/object-capacity/src/scan.rs b/crates/object-capacity/src/scan.rs index ed371e722..f274b25e1 100644 --- a/crates/object-capacity/src/scan.rs +++ b/crates/object-capacity/src/scan.rs @@ -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>, 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) -> 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 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; diff --git a/crates/scanner/src/scanner_folder.rs b/crates/scanner/src/scanner_folder.rs index 307f3e87d..d924ef187 100644 --- a/crates/scanner/src/scanner_folder.rs +++ b/crates/scanner/src/scanner_folder.rs @@ -463,14 +463,14 @@ fn build_object_heal_request( fn resolve_object_heal_entry(entries: &MetaCacheEntries, resolver: MetadataResolutionParams) -> Option { 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/");