mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +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 }
|
serial_test = { workspace = true }
|
||||||
temp-env = { workspace = true, features = ["async_closure"] }
|
temp-env = { workspace = true, features = ["async_closure"] }
|
||||||
tempfile = { workspace = true }
|
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,
|
record_capacity_stall_detected, record_capacity_symlink, record_capacity_timeout_fallback,
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
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 std::time::{Duration, Instant};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
use walkdir::WalkDir;
|
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_METADATA_FAILED: &str = "capacity_scan_metadata_failed";
|
||||||
const EVENT_CAPACITY_SCAN_SAMPLING_APPLIED: &str = "capacity_scan_sampling_applied";
|
const EVENT_CAPACITY_SCAN_SAMPLING_APPLIED: &str = "capacity_scan_sampling_applied";
|
||||||
const EVENT_CAPACITY_SCAN_EXACT_COMPLETED: &str = "capacity_scan_exact_completed";
|
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)]
|
#[derive(Debug)]
|
||||||
struct DiskScanOutcome {
|
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 {
|
async fn scan_disk_used_capacity(disk: CapacityDiskRef) -> DiskScanOutcome {
|
||||||
let disk_label = disk_metric_label(&disk);
|
let disk_label = disk_metric_label(&disk);
|
||||||
let drive_path = disk.drive_path.clone();
|
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() {
|
let metadata = match entry.metadata() {
|
||||||
Ok(meta) => meta,
|
Ok(meta) => meta,
|
||||||
Err(err) => {
|
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!(
|
warn!(
|
||||||
event = EVENT_CAPACITY_SCAN_METADATA_FAILED,
|
event = EVENT_CAPACITY_SCAN_METADATA_FAILED,
|
||||||
component = LOG_COMPONENT_CAPACITY,
|
component = LOG_COMPONENT_CAPACITY,
|
||||||
@@ -902,6 +941,67 @@ mod tests {
|
|||||||
assert!(result.is_err());
|
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]
|
#[tokio::test]
|
||||||
async fn test_calculate_data_dir_used_capacity_returns_partial_success() {
|
async fn test_calculate_data_dir_used_capacity_returns_partial_success() {
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|||||||
@@ -463,14 +463,14 @@ fn build_object_heal_request(
|
|||||||
|
|
||||||
fn resolve_object_heal_entry(entries: &MetaCacheEntries, resolver: MetadataResolutionParams) -> Option<MetaCacheEntry> {
|
fn resolve_object_heal_entry(entries: &MetaCacheEntries, resolver: MetadataResolutionParams) -> Option<MetaCacheEntry> {
|
||||||
if let Some(entry) = entries.resolve(resolver) {
|
if let Some(entry) = entries.resolve(resolver) {
|
||||||
return Some(entry);
|
return entry.is_object().then_some(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
entries
|
entries
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.find(|entry| !entry.name.ends_with(SLASH_SEPARATOR))
|
.find(|entry| entry.is_object() && !entry.name.ends_with(SLASH_SEPARATOR))
|
||||||
.cloned()
|
.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]
|
#[test]
|
||||||
fn test_resolve_object_heal_entry_uses_plain_fallback_after_trailing_slash() {
|
fn test_resolve_object_heal_entry_uses_plain_fallback_after_trailing_slash() {
|
||||||
let entries = MetaCacheEntries(vec![
|
let entries = MetaCacheEntries(vec![
|
||||||
@@ -3141,6 +3183,27 @@ mod tests {
|
|||||||
assert_eq!(entry.name, "object");
|
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]
|
#[test]
|
||||||
fn test_resolve_object_heal_entry_preserves_resolved_trailing_slash_object() {
|
fn test_resolve_object_heal_entry_preserves_resolved_trailing_slash_object() {
|
||||||
let metadata = metadata_for_object("bucket", "object/");
|
let metadata = metadata_for_object("bucket", "object/");
|
||||||
|
|||||||
Reference in New Issue
Block a user