fix(scanner): refresh scope safety independently of idle backoff

Inspect maintenance on multi-disk startup and refresh changed or failed
evidence even when explicit bitrot configuration disables idle backoff.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
houseme
2026-09-05 18:38:17 +08:00
parent 527860d71e
commit f9dd0388fa
2 changed files with 96 additions and 24 deletions
+26 -24
View File
@@ -1086,6 +1086,12 @@ impl ScannerMaintenanceFeatures {
fn needs_regular_cycle(self) -> bool {
self.lifecycle || self.replication || self.inspection_failed
}
fn requires_full_scan(self, observed_generation: Option<u64>, current_generation: u64, wake: ScannerCycleWakeReason) -> bool {
self.needs_regular_cycle()
|| observed_generation != Some(current_generation)
|| !matches!(wake, ScannerCycleWakeReason::DirtyUsage | ScannerCycleWakeReason::ClusterActivity)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1283,18 +1289,18 @@ async fn configure_scanner_defaults(
ctx: &CancellationToken,
storeapi: &Arc<impl ScannerStorage>,
) -> (ScannerMaintenanceFeatures, Option<u64>) {
let (features, maintenance_generation) = detect_stable_scanner_maintenance_features(ctx, storeapi)
.await
.unwrap_or_else(|| {
(
ScannerMaintenanceFeatures {
inspection_failed: true,
..Default::default()
},
scanner_maintenance_generation(),
)
});
if storeapi.setup_is_erasure_sd().await {
let (features, maintenance_generation) = detect_stable_scanner_maintenance_features(ctx, storeapi)
.await
.unwrap_or_else(|| {
(
ScannerMaintenanceFeatures {
inspection_failed: true,
..Default::default()
},
scanner_maintenance_generation(),
)
});
// Single-disk keeps the speed-preset-derived default cycle (60s at the
// `default` preset) instead of a special shorter cycle: no measured
// cold-start ILM latency basis for an override, and clean-idle backoff
@@ -1319,7 +1325,7 @@ async fn configure_scanner_defaults(
} else {
set_scanner_default_speed(ScannerSpeed::Default);
set_scanner_default_cycle_secs(None);
(ScannerMaintenanceFeatures::default(), None)
(features, Some(maintenance_generation))
}
}
@@ -2565,10 +2571,7 @@ where
let mut superseded_backoff = ScannerRetryBackoff::default();
let mut deferred_backoff = ScannerRetryBackoff::default();
let initial_runtime_config = resolve_scanner_runtime_config();
if clean_idle_topology_supported
&& scanner_clean_idle_backoff_configured(&initial_runtime_config)
&& maintenance_generation_seen.is_none()
{
if clean_idle_topology_supported && maintenance_generation_seen.is_none() {
let Some((features, generation)) = detect_stable_scanner_maintenance_features(&ctx, &storeapi).await else {
global_metrics().set_cycle(None).await;
finish_scanner_leader_iteration(false, "stopped", String::new()).await;
@@ -2871,7 +2874,7 @@ where
#[cfg(test)]
notify_scanner_runtime_observed_for_test(&storeapi, pause_backlog_observation);
let runtime_config = resolve_scanner_runtime_config();
if clean_idle_topology_supported && scanner_clean_idle_backoff_configured(&runtime_config) {
if clean_idle_topology_supported {
let current_generation = scanner_maintenance_generation();
if maintenance_generation_seen != Some(current_generation) {
scanner_activity_seen = None;
@@ -3067,9 +3070,11 @@ where
&mut cycle_revision,
leader_epoch,
cycle_budget.clone(),
maintenance_features.needs_regular_cycle()
|| maintenance_generation_seen != Some(scanner_maintenance_generation())
|| !matches!(wake_reason, ScannerCycleWakeReason::DirtyUsage | ScannerCycleWakeReason::ClusterActivity),
maintenance_features.requires_full_scan(
maintenance_generation_seen,
scanner_maintenance_generation(),
wake_reason,
),
),
guard.lock_lost_notified(),
)
@@ -3127,10 +3132,7 @@ where
let maintenance_config_changed =
maintenance_generation_seen.is_some_and(|generation| generation != current_maintenance_generation);
let retry_failed_inspection = maintenance_inspection_retry.retry_due(maintenance_features, wake_reason, Instant::now());
if clean_idle_topology_supported
&& scanner_clean_idle_backoff_configured(&runtime_config)
&& (maintenance_config_changed || retry_failed_inspection)
{
if clean_idle_topology_supported && (maintenance_config_changed || retry_failed_inspection) {
let Some((features, generation)) = detect_stable_scanner_maintenance_features(&ctx, &storeapi).await else {
break;
};
+70
View File
@@ -8127,6 +8127,76 @@ fn clean_idle_backoff_policy_preserves_explicit_and_maintenance_cycles() {
}
}
#[tokio::test]
#[serial]
async fn scoped_scan_explicit_bitrot_keeps_dirty_planning_without_idle_backoff() {
temp_env::async_with_vars([(ENV_SCANNER_CYCLE, None), (ENV_SCANNER_BITROT_CYCLE_SECS, Some("3600"))], async {
crate::runtime_config::refresh_scanner_runtime_config_for_tests();
crate::scanner_io::clear_dirty_usage_buckets_for_tests();
let (_temp_dir, store) = setup_scanner_cycle_store_with_pool_count(true, 2).await;
let ctx = CancellationToken::new();
let (features, generation) = configure_scanner_defaults(&ctx, &store).await;
let config = resolve_scanner_runtime_config();
assert_eq!(config.cycle_interval_source, ScannerRuntimeConfigSource::Default);
assert_eq!(config.bitrot_cycle_source, ScannerRuntimeConfigSource::Env);
assert!(!scanner_clean_idle_backoff_configured(&config));
assert!(!features.needs_regular_cycle());
assert_eq!(
generation,
Some(scanner_maintenance_generation()),
"multi-disk startup must inspect maintenance independently"
);
let observed = ScannerCycleObservedGenerations::for_wait(&config, None, 7, 0, scanner_maintenance_generation());
assert_eq!(observed.dirty_usage, Some(7), "explicit bitrot still permits ordinary dirty wakeups");
for (wake, full) in [
(ScannerCycleWakeReason::DirtyUsage, false),
(ScannerCycleWakeReason::ClusterActivity, false),
(ScannerCycleWakeReason::Timer, true),
(ScannerCycleWakeReason::ClusterMaintenance, true),
] {
assert_eq!(
features.requires_full_scan(generation, scanner_maintenance_generation(), wake),
full,
"{wake:?}"
);
}
assert!(features.requires_full_scan(None, scanner_maintenance_generation(), ScannerCycleWakeReason::DirtyUsage));
for unsafe_features in [
ScannerMaintenanceFeatures {
lifecycle: true,
..Default::default()
},
ScannerMaintenanceFeatures {
replication: true,
..Default::default()
},
ScannerMaintenanceFeatures {
inspection_failed: true,
..Default::default()
},
] {
assert!(unsafe_features.requires_full_scan(
generation,
scanner_maintenance_generation(),
ScannerCycleWakeReason::DirtyUsage
));
}
crate::scanner_io::record_scanner_maintenance_change("maintenance-proof-change");
assert!(features.requires_full_scan(generation, scanner_maintenance_generation(), ScannerCycleWakeReason::DirtyUsage));
let (refreshed, refreshed_generation) = detect_stable_scanner_maintenance_features(&ctx, &store)
.await
.expect("changed maintenance generation should be inspected");
assert!(!refreshed.requires_full_scan(
Some(refreshed_generation),
scanner_maintenance_generation(),
ScannerCycleWakeReason::DirtyUsage
));
crate::scanner_io::clear_dirty_usage_buckets_for_tests();
})
.await;
crate::runtime_config::refresh_scanner_runtime_config_for_tests();
}
#[test]
fn clean_idle_backoff_requires_activity_probes() {
let default_config = ScannerRuntimeConfig::default();