fix(scanner): ignore missing rebalance metadata (#3282)

* fix(ecstore): harden rebalance data movement

* fix(ecstore): preserve failed rebalance status

* fix(ecstore): avoid rebalance walkdir total timeout

* fix(ecstore): retry rebalance listing timeouts

* fix(ecstore): restrict data movement resume target

* refactor(ecstore): simplify multipart movement target

* fix(ecstore): restore resume target checks

* perf(ecstore): speed up rebalance bucket merges

* fix: keep rebalance listing alive on transient failures

* fix(scanner): ignore missing rebalance metadata

---------

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
cxymds
2026-06-08 22:24:16 +08:00
committed by GitHub
parent 8c3e52efb8
commit 452c003341
4 changed files with 75 additions and 12 deletions
+1 -4
View File
@@ -3995,10 +3995,7 @@ impl HealOperations for SetDisks {
let disks = disks.clone();
let (_, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, version_id, false, false, false).await?;
if DiskError::is_all_not_found(&errs) {
warn!(
"heal_object failed, all obj part not found, bucket: {}, obj: {}, version_id: {}",
bucket, object, version_id
);
debug!(bucket, object, version_id, "heal_object skipped missing object");
let err = if !version_id.is_empty() {
Error::FileVersionNotFound
} else {
+1 -4
View File
@@ -69,10 +69,7 @@ impl SetDisks {
"File info read complete"
);
if DiskError::is_all_not_found(&errs) {
warn!(
"heal_object failed, all obj part not found, bucket: {}, obj: {}, version_id: {}",
bucket, object, version_id
);
debug!(bucket, object, version_id, "heal_object skipped missing object");
let err = if !version_id.is_empty() {
DiskError::FileVersionNotFound
} else {
+2 -2
View File
@@ -28,7 +28,7 @@ use crate::runtime_config::{
scanner_alert_excess_folders, scanner_alert_excess_version_size, scanner_alert_excess_versions, scanner_yield_every_n_objects,
};
use crate::scanner_budget::{ScannerCycleBudget, ScannerCycleBudgetReason};
use crate::scanner_io::ScannerIODisk as _;
use crate::scanner_io::{SCANNER_SKIP_FILE_ERROR, ScannerIODisk as _};
use crate::sleeper::DynamicSleeper;
use metrics::{counter, describe_counter};
use rustfs_common::heal_channel::{
@@ -1286,7 +1286,7 @@ impl FolderScanner {
let sz = match self.local_disk.get_size(item.clone()).await {
Ok(sz) => sz,
Err(e) => {
let is_skip_file = matches!(e, StorageError::Io(ref io) if io.to_string() == "skip file");
let is_skip_file = matches!(e, StorageError::Io(ref io) if io.to_string() == SCANNER_SKIP_FILE_ERROR);
if !is_skip_file {
// Track failed objects to prevent infinite retry loops
+71 -2
View File
@@ -35,6 +35,7 @@ use rustfs_ecstore::bucket::versioning::VersioningApi as _;
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
use rustfs_ecstore::config::storageclass;
use rustfs_ecstore::disk::STORAGE_FORMAT_FILE;
use rustfs_ecstore::disk::error::DiskError;
use rustfs_ecstore::disk::{Disk, DiskAPI};
use rustfs_ecstore::error::{Error, StorageError};
use rustfs_ecstore::global::GLOBAL_TierConfigMgr;
@@ -56,6 +57,8 @@ use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, warn};
pub(crate) const SCANNER_SKIP_FILE_ERROR: &str = "skip file";
const METRIC_SCANNER_SET_SCAN_CONCURRENCY_LIMIT: &str = "rustfs_scanner_set_scan_concurrency_limit";
const METRIC_SCANNER_DISK_SCAN_CONCURRENCY_LIMIT: &str = "rustfs_scanner_disk_scan_concurrency_limit";
const METRIC_SCANNER_SET_SCAN_WAIT_SECONDS: &str = "rustfs_scanner_set_scan_wait_seconds";
@@ -939,11 +942,14 @@ impl ScannerIODisk for Disk {
let done_object = Metrics::time(Metric::ScanObject);
if !is_xl_meta_path(&item.path) {
return Err(StorageError::other("skip file".to_string()));
return Err(StorageError::other(SCANNER_SKIP_FILE_ERROR.to_string()));
}
let data = match self.read_metadata(&item.bucket, &item.object_path()).await {
Ok(data) => data,
Err(e) if DiskError::is_err_object_not_found(&e) || DiskError::is_err_version_not_found(&e) => {
return Err(StorageError::other(SCANNER_SKIP_FILE_ERROR.to_string()));
}
Err(e) => {
return Err(StorageError::other(format!(
"failed to read metadata: {e}, bucket={}, object_path={}",
@@ -960,7 +966,7 @@ impl ScannerIODisk for Disk {
Ok(versions) => versions,
Err(e) => {
error!("Failed to get file info versions: {}/{}, err: {e}", item.bucket, item.object_path());
return Err(StorageError::other("skip file".to_string()));
return Err(StorageError::other(SCANNER_SKIP_FILE_ERROR.to_string()));
}
};
@@ -1127,8 +1133,12 @@ impl ScannerIODisk for Disk {
#[cfg(test)]
mod tests {
use super::*;
use crate::scanner_folder::ScannerItem;
use rustfs_ecstore::disk::{DiskOption, STORAGE_FORMAT_FILE, endpoint::Endpoint, new_disk};
use rustfs_ecstore::pools::path2_bucket_object_with_base_path;
use serial_test::serial;
use temp_env::with_var;
use uuid::Uuid;
#[test]
fn record_set_scan_failure_preserves_first_error() {
@@ -1216,6 +1226,65 @@ mod tests {
assert!(is_xl_meta_path("/data/bucket/object/xl.meta"));
}
#[tokio::test]
async fn get_size_treats_missing_metadata_as_skip_file() {
let temp_dir = std::env::temp_dir().join(format!("rustfs-scanner-missing-meta-{}", Uuid::new_v4()));
let bucket = "bucket";
let object = "object";
let object_dir = temp_dir.join(bucket).join(object);
let metadata_path = object_dir.join(STORAGE_FORMAT_FILE);
tokio::fs::create_dir_all(&object_dir)
.await
.expect("failed to create object directory");
tokio::fs::write(&metadata_path, [])
.await
.expect("failed to create metadata placeholder");
let endpoint = Endpoint::try_from(temp_dir.to_string_lossy().as_ref()).expect("failed to create endpoint");
let disk = new_disk(
&endpoint,
&DiskOption {
cleanup: false,
health_check: false,
},
)
.await
.expect("failed to open local disk");
let relative_path = metadata_path.to_string_lossy().to_string();
let (_, scanner_path) = path2_bucket_object_with_base_path(temp_dir.to_string_lossy().as_ref(), relative_path.as_str());
let file_type = tokio::fs::metadata(&metadata_path)
.await
.expect("failed to stat metadata placeholder")
.file_type();
tokio::fs::remove_dir_all(&object_dir)
.await
.expect("failed to remove object directory");
let item = ScannerItem {
path: scanner_path,
bucket: bucket.to_string(),
prefix: object.to_string(),
object_name: STORAGE_FORMAT_FILE.to_string(),
file_type,
lifecycle: None,
replication: None,
heal_enabled: false,
heal_bitrot: false,
debug: false,
};
let err = disk
.get_size(item)
.await
.expect_err("missing metadata should be skipped instead of reported as a scanner failure");
assert!(matches!(err, StorageError::Io(ref io) if io.to_string() == SCANNER_SKIP_FILE_ERROR));
let _ = tokio::fs::remove_dir_all(&temp_dir).await;
}
#[test]
fn cache_root_entry_info_flattens_bucket_children() {
let mut cache = DataUsageCache {