feat(scanner): wake cycles for dirty usage refresh (#3617)

This commit is contained in:
Henry Guo
2026-06-19 18:36:48 +08:00
committed by GitHub
parent 54ffbedbc8
commit a689ff3c39
6 changed files with 84 additions and 8 deletions
+55 -6
View File
@@ -23,7 +23,7 @@ use crate::runtime_config::{
};
use crate::scanner_budget::{ScannerCycleBudget, ScannerCycleBudgetConfig, ScannerCycleBudgetReason};
use crate::scanner_folder::{data_usage_update_dir_cycles, heal_object_select_prob};
use crate::scanner_io::ScannerIO;
use crate::scanner_io::{ScannerIO, dirty_usage_bucket_notified, dirty_usage_buckets_pending};
use crate::sleeper::{SCANNER_SLEEPER, set_scanner_default_speed};
use crate::{DataUsageInfo, ScannerActivityGuard, ScannerError};
use chrono::{DateTime, Utc};
@@ -131,6 +131,30 @@ fn randomized_cycle_delay_for(interval: Duration) -> Duration {
delay.max(Duration::from_secs(1))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ScannerCycleWakeReason {
Timer,
DirtyUsage,
Cancelled,
}
async fn wait_for_next_scanner_cycle(ctx: &CancellationToken, delay: Duration) -> ScannerCycleWakeReason {
let sleep = tokio::time::sleep(delay);
tokio::pin!(sleep);
loop {
tokio::select! {
_ = ctx.cancelled() => return ScannerCycleWakeReason::Cancelled,
_ = &mut sleep => return ScannerCycleWakeReason::Timer,
_ = dirty_usage_bucket_notified() => {
if dirty_usage_buckets_pending() {
return ScannerCycleWakeReason::DirtyUsage;
}
}
}
}
}
fn initial_scanner_delay_for(start_delay_secs: Option<u64>) -> Duration {
start_delay_secs
.map(|secs| randomized_cycle_delay_for(Duration::from_secs(secs)))
@@ -932,12 +956,22 @@ pub async fn run_data_scanner(ctx: CancellationToken, storeapi: Arc<ECStore>) ->
break;
}
// Randomized inter-cycle delay
tokio::select! {
_ = ctx.cancelled() => break,
_ = tokio::time::sleep(randomized_cycle_delay()) => {
match wait_for_next_scanner_cycle(&ctx, randomized_cycle_delay()).await {
ScannerCycleWakeReason::Cancelled => break,
ScannerCycleWakeReason::Timer => {
run_data_scanner_cycle(&ctx, &storeapi, &mut cycle_info).await;
},
}
ScannerCycleWakeReason::DirtyUsage => {
debug!(
target: "rustfs::scanner",
event = EVENT_SCANNER_CYCLE_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_RUNTIME,
state = "dirty_usage_wakeup",
"Scanner cycle woke for dirty usage work"
);
run_data_scanner_cycle(&ctx, &storeapi, &mut cycle_info).await;
}
}
}
@@ -1590,6 +1624,21 @@ mod tests {
assert!(delay < Duration::from_secs(2), "expected delay < 2s");
}
#[tokio::test]
#[serial]
async fn test_wait_for_next_scanner_cycle_wakes_for_dirty_usage() {
crate::scanner_io::clear_dirty_usage_bucket("photos");
crate::scanner_io::record_dirty_usage_bucket("photos");
let ctx = CancellationToken::new();
let reason = tokio::time::timeout(Duration::from_secs(1), wait_for_next_scanner_cycle(&ctx, Duration::from_secs(60)))
.await
.expect("dirty usage should wake scanner before timer");
assert_eq!(reason, ScannerCycleWakeReason::DirtyUsage);
crate::scanner_io::clear_dirty_usage_bucket("photos");
}
#[test]
#[serial]
fn test_get_cycle_scan_mode_runs_deep_until_selection_window_completes() {
+11 -1
View File
@@ -37,7 +37,7 @@ use std::sync::{LazyLock, Mutex as StdMutex, MutexGuard};
use std::time::{Instant, SystemTime};
use std::{fmt::Debug, sync::Arc};
use time::OffsetDateTime;
use tokio::sync::{Mutex, Semaphore, mpsc};
use tokio::sync::{Mutex, Notify, Semaphore, mpsc};
use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, warn};
@@ -86,6 +86,7 @@ impl ScannerBucketScanPlan {
static DIRTY_USAGE_BUCKET_GENERATION: AtomicU64 = AtomicU64::new(0);
static DIRTY_USAGE_BUCKETS: LazyLock<StdMutex<DirtyUsageBuckets>> = LazyLock::new(|| StdMutex::new(HashMap::new()));
static DIRTY_USAGE_BUCKET_NOTIFY: LazyLock<Notify> = LazyLock::new(Notify::new);
fn dirty_usage_buckets() -> MutexGuard<'static, DirtyUsageBuckets> {
DIRTY_USAGE_BUCKETS.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
@@ -107,6 +108,7 @@ pub fn record_dirty_usage_bucket(bucket: &str) {
dirty_buckets.len()
};
global_metrics().record_scanner_dirty_usage_pending(usize_to_u64_saturated(pending_buckets));
DIRTY_USAGE_BUCKET_NOTIFY.notify_one();
}
pub fn clear_dirty_usage_bucket(bucket: &str) {
@@ -138,6 +140,14 @@ fn snapshot_dirty_usage_buckets(buckets: &[BucketInfo]) -> DirtyUsageBuckets {
snapshot
}
pub(crate) fn dirty_usage_buckets_pending() -> bool {
!dirty_usage_buckets().is_empty()
}
pub(crate) async fn dirty_usage_bucket_notified() {
DIRTY_USAGE_BUCKET_NOTIFY.notified().await;
}
fn clear_dirty_usage_buckets(snapshot: &DirtyUsageBuckets) {
let (cleared_buckets, pending_buckets) = {
let mut dirty_buckets = dirty_usage_buckets();