mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
fix(scanner): reduce non-actionable scan noise (#3840)
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user