diff --git a/crates/scanner/src/scanner.rs b/crates/scanner/src/scanner.rs index 62a3d0527..fcf67eed0 100644 --- a/crates/scanner/src/scanner.rs +++ b/crates/scanner/src/scanner.rs @@ -2895,7 +2895,7 @@ where ); let mut wait_plan = scanner_cycle_wait_plan(&runtime_config, clean_idle_backoff, backoff_enabled, randomized_cycle_delay_for); - let superseded_retry_interval = superseded_backoff.retry_interval(runtime_config.cycle_interval); + let superseded_retry_interval = scanner_superseded_retry_interval(superseded_backoff, &runtime_config); let deferred_retry_interval = deferred_backoff.retry_interval(runtime_config.cycle_interval); let mut convergence_retry_interval = superseded_retry_interval.or(deferred_retry_interval); if let Some(retry_interval) = convergence_retry_interval { diff --git a/crates/scanner/src/scanner/activity.rs b/crates/scanner/src/scanner/activity.rs index 815679c01..2f8aed1b5 100644 --- a/crates/scanner/src/scanner/activity.rs +++ b/crates/scanner/src/scanner/activity.rs @@ -109,6 +109,22 @@ impl ScannerRetryBackoff { } } +pub(super) fn scanner_superseded_retry_interval( + backoff: ScannerRetryBackoff, + runtime_config: &ScannerRuntimeConfig, +) -> Option { + let retry_interval = backoff.retry_interval(runtime_config.cycle_interval)?; + if runtime_config.cycle_interval_source == ScannerRuntimeConfigSource::Default { + return Some(retry_interval); + } + + // An explicit cycle is an operator-selected duty-cycle floor. A + // superseded snapshot keeps its dirty work pending, but retrying that work + // sooner than the configured cadence would turn continuous writes into a + // repeated full-walk loop despite the override. + Some(retry_interval.max(runtime_config.cycle_interval)) +} + const SCANNER_PUBLICATION_PROOF_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(30); pub(crate) fn scanner_publication_proof_retry_delay(consecutive_failures: u32) -> Duration { diff --git a/crates/scanner/src/scanner/tests.rs b/crates/scanner/src/scanner/tests.rs index d9abe98e6..efe29f612 100644 --- a/crates/scanner/src/scanner/tests.rs +++ b/crates/scanner/src/scanner/tests.rs @@ -7197,6 +7197,40 @@ fn superseded_retry_backoff_grows_from_the_default_cycle() { } } +#[test] +fn superseded_retry_preserves_explicit_cycle_cadence() { + let mut backoff = ScannerRetryBackoff::default(); + backoff.record_retryable_cycle(true); + + let default_config = ScannerRuntimeConfig { + cycle_interval: Duration::from_secs(6 * 60 * 60), + ..Default::default() + }; + assert_eq!( + scanner_superseded_retry_interval(backoff, &default_config), + Some(SCANNER_RETRY_BASE_INTERVAL), + "the default adaptive cadence must retain fast convergence" + ); + + for source in [ + ScannerRuntimeConfigSource::Env, + ScannerRuntimeConfigSource::Config, + ScannerRuntimeConfigSource::ScannerCompatConfig, + ] { + let runtime_config = ScannerRuntimeConfig { + cycle_interval: Duration::from_secs(6 * 60 * 60), + cycle_interval_source: source, + ..Default::default() + }; + + assert_eq!( + scanner_superseded_retry_interval(backoff, &runtime_config), + Some(runtime_config.cycle_interval), + "{source:?} cycle cadence must not be shortened by convergence retries" + ); + } +} + #[test] fn publication_proof_retry_backoff_reaches_its_short_cap() { for (failures, expected) in [(1, 5), (2, 10), (3, 20), (4, 30), (20, 30)] {