From 923f86f2aa81810f3af9af47ce96f3b5c7282624 Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 30 Aug 2026 00:12:18 +0800 Subject: [PATCH] fix(scanner): fence legacy usage floor fallback Co-Authored-By: heihutu --- crates/scanner/src/scanner/cycle_state.rs | 12 ++++++++-- crates/scanner/src/scanner/tests.rs | 28 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/scanner/src/scanner/cycle_state.rs b/crates/scanner/src/scanner/cycle_state.rs index 133b5f933..95a42e37f 100644 --- a/crates/scanner/src/scanner/cycle_state.rs +++ b/crates/scanner/src/scanner/cycle_state.rs @@ -1347,6 +1347,7 @@ pub(super) async fn persisted_usage_floor_for_startup( }; for primary_path in [DATA_USAGE_OBJ_NAME_PATH.as_str(), LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str()] { let backup_path = format!("{primary_path}.bkp"); + let is_v2_path = primary_path == DATA_USAGE_OBJ_NAME_PATH.as_str(); let primary_epoch = match read_config_with_revision(storeapi.clone(), primary_path).await { Ok((Some(data), _)) => { let usage = serde_json::from_slice::(&data).map_err(|err| { @@ -1365,8 +1366,15 @@ pub(super) async fn persisted_usage_floor_for_startup( None } else { let epoch = usage.scanner_epoch.unwrap_or_default(); - update_floor(&mut floor, &usage, primary_path)?; - Some(epoch) + // A legacy snapshot may be structurally valid but older + // than an incomplete v2 snapshot left by a newer leader. + // Do not let that candidate regress the startup floor. + if !is_v2_path && invalid_baseline_epoch.is_some_and(|fenced_epoch| epoch < fenced_epoch) { + None + } else { + update_floor(&mut floor, &usage, primary_path)?; + Some(epoch) + } } } Ok((None, _)) => None, diff --git a/crates/scanner/src/scanner/tests.rs b/crates/scanner/src/scanner/tests.rs index c745c4c82..1aae284ee 100644 --- a/crates/scanner/src/scanner/tests.rs +++ b/crates/scanner/src/scanner/tests.rs @@ -2038,6 +2038,34 @@ async fn scanner_usage_floor_rejects_backup_older_than_incomplete_v2_primary() { assert!(err.to_string().contains("no authoritative baseline")); } +#[tokio::test] +async fn scanner_usage_floor_rejects_older_legacy_primary_after_incomplete_v2_primary() { + let store = Arc::new(MemoryConfigStore::default()); + let primary = DataUsageInfo { + scanner_epoch: Some(7), + scanner_cycle: Some(100), + usage_snapshot_complete: false, + ..Default::default() + }; + let mut legacy = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH), 0); + legacy.scanner_epoch = Some(6); + legacy.scanner_cycle = Some(103); + for (path, usage) in [ + (DATA_USAGE_OBJ_NAME_PATH.as_str(), primary), + (LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str(), legacy), + ] { + store.objects.lock().await.insert( + memory_config_key(RUSTFS_META_BUCKET, path), + serde_json::to_vec(&usage).expect("usage snapshot should encode"), + ); + } + + let err = persisted_usage_floor_for_startup(store, true) + .await + .expect_err("an older legacy baseline must not cross the incomplete v2 epoch fence"); + assert!(err.to_string().contains("no authoritative baseline")); +} + #[tokio::test] async fn scanner_leadership_fencing_recovers_incomplete_v2_primary_from_backup() { let store = Arc::new(MemoryConfigStore::default());