feat(scanner): expose scan partial source status (#3247)

* feat(scanner): expose scan partial source status

* fix(scanner): expose partial source in aggregated metrics

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Henry Guo
2026-06-07 14:40:04 +08:00
committed by GitHub
parent f8aa4fa221
commit 4ae070c8cc
4 changed files with 258 additions and 7 deletions
+104 -2
View File
@@ -267,6 +267,31 @@ impl ScannerWorkSource {
} }
} }
fn code(self) -> u8 {
match self {
Self::Usage => 1,
Self::Lifecycle => 2,
Self::BucketReplication => 3,
Self::SiteReplication => 4,
Self::Heal => 5,
Self::Bitrot => 6,
Self::Alerts => 7,
}
}
fn from_code(code: u8) -> Option<Self> {
match code {
1 => Some(Self::Usage),
2 => Some(Self::Lifecycle),
3 => Some(Self::BucketReplication),
4 => Some(Self::SiteReplication),
5 => Some(Self::Heal),
6 => Some(Self::Bitrot),
7 => Some(Self::Alerts),
_ => None,
}
}
fn all() -> &'static [Self] { fn all() -> &'static [Self] {
&Self::ALL &Self::ALL
} }
@@ -521,6 +546,7 @@ pub struct Metrics {
scanner_disk_bucket_scan_states: Mutex<HashMap<String, ScannerDiskBucketScanState>>, scanner_disk_bucket_scan_states: Mutex<HashMap<String, ScannerDiskBucketScanState>>,
last_scan_cycle_result: AtomicU8, last_scan_cycle_result: AtomicU8,
last_scan_cycle_partial_reason: AtomicU8, last_scan_cycle_partial_reason: AtomicU8,
last_scan_cycle_partial_source: AtomicU8,
last_scan_cycle_duration_millis: AtomicU64, last_scan_cycle_duration_millis: AtomicU64,
last_scan_cycle_objects_scanned: AtomicU64, last_scan_cycle_objects_scanned: AtomicU64,
last_scan_cycle_directories_scanned: AtomicU64, last_scan_cycle_directories_scanned: AtomicU64,
@@ -539,6 +565,7 @@ pub struct Metrics {
partial_scan_cycles_runtime: AtomicU64, partial_scan_cycles_runtime: AtomicU64,
partial_scan_cycles_objects: AtomicU64, partial_scan_cycles_objects: AtomicU64,
partial_scan_cycles_directories: AtomicU64, partial_scan_cycles_directories: AtomicU64,
partial_scan_cycles_by_source: Vec<AtomicU64>,
scanner_yield_duration_millis: AtomicU64, scanner_yield_duration_millis: AtomicU64,
scanner_throttle_sleep_duration_millis: AtomicU64, scanner_throttle_sleep_duration_millis: AtomicU64,
scanner_ilm_actions: AtomicU64, scanner_ilm_actions: AtomicU64,
@@ -650,6 +677,12 @@ pub struct ScannerSourceWorkSnapshot {
pub skipped: u64, pub skipped: u64,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScannerSourceCycleSnapshot {
pub source: String,
pub cycles: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ScannerLastMinute { pub struct ScannerLastMinute {
pub actions: HashMap<String, ScannerTimedAction>, pub actions: HashMap<String, ScannerTimedAction>,
@@ -714,6 +747,10 @@ pub struct ScannerMetricsReport {
pub last_cycle_partial_reason: String, pub last_cycle_partial_reason: String,
#[serde(default)] #[serde(default)]
pub last_cycle_partial_reason_code: u64, pub last_cycle_partial_reason_code: u64,
#[serde(default)]
pub last_cycle_partial_source: String,
#[serde(default)]
pub last_cycle_partial_source_code: u64,
pub last_cycle_duration_seconds: f64, pub last_cycle_duration_seconds: f64,
#[serde(default)] #[serde(default)]
pub last_cycle_objects_scanned: u64, pub last_cycle_objects_scanned: u64,
@@ -749,6 +786,8 @@ pub struct ScannerMetricsReport {
#[serde(default)] #[serde(default)]
pub partial_cycles_directories: u64, pub partial_cycles_directories: u64,
#[serde(default)] #[serde(default)]
pub partial_cycles_by_source: Vec<ScannerSourceCycleSnapshot>,
#[serde(default)]
pub throttle_idle_mode_enabled: bool, pub throttle_idle_mode_enabled: bool,
#[serde(default)] #[serde(default)]
pub throttle_sleep_factor: f64, pub throttle_sleep_factor: f64,
@@ -863,6 +902,15 @@ pub fn emit_scan_cycle_partial(duration: Duration, reason: ScanCyclePartialReaso
metrics::counter!(OTEL_SCANNER_CYCLES, "result" => SCAN_CYCLE_RESULT_PARTIAL_LABEL).increment(1); metrics::counter!(OTEL_SCANNER_CYCLES, "result" => SCAN_CYCLE_RESULT_PARTIAL_LABEL).increment(1);
} }
pub fn emit_scan_cycle_partial_with_source(
duration: Duration,
reason: ScanCyclePartialReason,
source: Option<ScannerWorkSource>,
) {
global_metrics().record_scan_cycle_partial_with_source(duration, reason, source);
metrics::counter!(OTEL_SCANNER_CYCLES, "result" => SCAN_CYCLE_RESULT_PARTIAL_LABEL).increment(1);
}
pub fn emit_scan_bucket_drive_complete(success: bool, bucket: &str, disk: &str, duration: Duration) { pub fn emit_scan_bucket_drive_complete(success: bool, bucket: &str, disk: &str, duration: Duration) {
let result = if success { "success" } else { "error" }; let result = if success { "success" } else { "error" };
metrics::counter!( metrics::counter!(
@@ -931,6 +979,7 @@ impl Metrics {
scanner_disk_bucket_scan_states: Mutex::new(HashMap::new()), scanner_disk_bucket_scan_states: Mutex::new(HashMap::new()),
last_scan_cycle_result: AtomicU8::new(SCAN_CYCLE_RESULT_UNKNOWN), last_scan_cycle_result: AtomicU8::new(SCAN_CYCLE_RESULT_UNKNOWN),
last_scan_cycle_partial_reason: AtomicU8::new(ScanCyclePartialReason::Unknown as u8), last_scan_cycle_partial_reason: AtomicU8::new(ScanCyclePartialReason::Unknown as u8),
last_scan_cycle_partial_source: AtomicU8::new(0),
last_scan_cycle_duration_millis: AtomicU64::new(0), last_scan_cycle_duration_millis: AtomicU64::new(0),
last_scan_cycle_objects_scanned: AtomicU64::new(0), last_scan_cycle_objects_scanned: AtomicU64::new(0),
last_scan_cycle_directories_scanned: AtomicU64::new(0), last_scan_cycle_directories_scanned: AtomicU64::new(0),
@@ -949,6 +998,7 @@ impl Metrics {
partial_scan_cycles_runtime: AtomicU64::new(0), partial_scan_cycles_runtime: AtomicU64::new(0),
partial_scan_cycles_objects: AtomicU64::new(0), partial_scan_cycles_objects: AtomicU64::new(0),
partial_scan_cycles_directories: AtomicU64::new(0), partial_scan_cycles_directories: AtomicU64::new(0),
partial_scan_cycles_by_source: ScannerWorkSource::all().iter().map(|_| AtomicU64::new(0)).collect(),
scanner_yield_duration_millis: AtomicU64::new(0), scanner_yield_duration_millis: AtomicU64::new(0),
scanner_throttle_sleep_duration_millis: AtomicU64::new(0), scanner_throttle_sleep_duration_millis: AtomicU64::new(0),
scanner_ilm_actions: AtomicU64::new(0), scanner_ilm_actions: AtomicU64::new(0),
@@ -1358,11 +1408,21 @@ impl Metrics {
self.last_scan_cycle_result.store(result, Ordering::Relaxed); self.last_scan_cycle_result.store(result, Ordering::Relaxed);
self.last_scan_cycle_partial_reason self.last_scan_cycle_partial_reason
.store(ScanCyclePartialReason::Unknown as u8, Ordering::Relaxed); .store(ScanCyclePartialReason::Unknown as u8, Ordering::Relaxed);
self.last_scan_cycle_partial_source.store(0, Ordering::Relaxed);
self.last_scan_cycle_duration_millis self.last_scan_cycle_duration_millis
.store(duration_millis_saturated(duration), Ordering::Relaxed); .store(duration_millis_saturated(duration), Ordering::Relaxed);
} }
pub fn record_scan_cycle_partial(&self, duration: Duration, reason: ScanCyclePartialReason) { pub fn record_scan_cycle_partial(&self, duration: Duration, reason: ScanCyclePartialReason) {
self.record_scan_cycle_partial_with_source(duration, reason, None);
}
pub fn record_scan_cycle_partial_with_source(
&self,
duration: Duration,
reason: ScanCyclePartialReason,
source: Option<ScannerWorkSource>,
) {
self.partial_scan_cycles.fetch_add(1, Ordering::Relaxed); self.partial_scan_cycles.fetch_add(1, Ordering::Relaxed);
match reason { match reason {
ScanCyclePartialReason::Unknown => &self.partial_scan_cycles_unknown, ScanCyclePartialReason::Unknown => &self.partial_scan_cycles_unknown,
@@ -1374,6 +1434,13 @@ impl Metrics {
self.last_scan_cycle_result self.last_scan_cycle_result
.store(SCAN_CYCLE_RESULT_PARTIAL, Ordering::Relaxed); .store(SCAN_CYCLE_RESULT_PARTIAL, Ordering::Relaxed);
self.last_scan_cycle_partial_reason.store(reason as u8, Ordering::Relaxed); self.last_scan_cycle_partial_reason.store(reason as u8, Ordering::Relaxed);
self.last_scan_cycle_partial_source
.store(source.map(ScannerWorkSource::code).unwrap_or_default(), Ordering::Relaxed);
if let Some(source) = source
&& let Some(cycles) = self.partial_scan_cycles_by_source.get(source.index())
{
cycles.fetch_add(1, Ordering::Relaxed);
}
self.last_scan_cycle_duration_millis self.last_scan_cycle_duration_millis
.store(duration_millis_saturated(duration), Ordering::Relaxed); .store(duration_millis_saturated(duration), Ordering::Relaxed);
} }
@@ -1647,6 +1714,12 @@ impl Metrics {
let partial_reason = ScanCyclePartialReason::from_code(self.last_scan_cycle_partial_reason.load(Ordering::Relaxed)); let partial_reason = ScanCyclePartialReason::from_code(self.last_scan_cycle_partial_reason.load(Ordering::Relaxed));
m.last_cycle_partial_reason = partial_reason.as_str().to_string(); m.last_cycle_partial_reason = partial_reason.as_str().to_string();
m.last_cycle_partial_reason_code = partial_reason as u8 as u64; m.last_cycle_partial_reason_code = partial_reason as u8 as u64;
let partial_source = ScannerWorkSource::from_code(self.last_scan_cycle_partial_source.load(Ordering::Relaxed));
m.last_cycle_partial_source = partial_source
.map(ScannerWorkSource::as_str)
.unwrap_or(SCAN_CYCLE_RESULT_UNKNOWN_LABEL)
.to_string();
m.last_cycle_partial_source_code = partial_source.map(|source| source.code().into()).unwrap_or_default();
m.last_cycle_duration_seconds = self.last_scan_cycle_duration_millis.load(Ordering::Relaxed) as f64 / 1000.0; m.last_cycle_duration_seconds = self.last_scan_cycle_duration_millis.load(Ordering::Relaxed) as f64 / 1000.0;
m.last_cycle_objects_scanned = self.last_scan_cycle_objects_scanned.load(Ordering::Relaxed); m.last_cycle_objects_scanned = self.last_scan_cycle_objects_scanned.load(Ordering::Relaxed);
m.last_cycle_directories_scanned = self.last_scan_cycle_directories_scanned.load(Ordering::Relaxed); m.last_cycle_directories_scanned = self.last_scan_cycle_directories_scanned.load(Ordering::Relaxed);
@@ -1667,6 +1740,17 @@ impl Metrics {
m.partial_cycles_runtime = self.partial_scan_cycles_runtime.load(Ordering::Relaxed); m.partial_cycles_runtime = self.partial_scan_cycles_runtime.load(Ordering::Relaxed);
m.partial_cycles_objects = self.partial_scan_cycles_objects.load(Ordering::Relaxed); m.partial_cycles_objects = self.partial_scan_cycles_objects.load(Ordering::Relaxed);
m.partial_cycles_directories = self.partial_scan_cycles_directories.load(Ordering::Relaxed); m.partial_cycles_directories = self.partial_scan_cycles_directories.load(Ordering::Relaxed);
m.partial_cycles_by_source = ScannerWorkSource::all()
.iter()
.filter_map(|source| {
self.partial_scan_cycles_by_source
.get(source.index())
.map(|cycles| ScannerSourceCycleSnapshot {
source: source.as_str().to_string(),
cycles: cycles.load(Ordering::Relaxed),
})
})
.collect();
m.throttle_idle_mode_enabled = self.scanner_throttle_idle_mode_enabled.load(Ordering::Relaxed); m.throttle_idle_mode_enabled = self.scanner_throttle_idle_mode_enabled.load(Ordering::Relaxed);
m.throttle_sleep_factor = self.scanner_throttle_sleep_factor_micros.load(Ordering::Relaxed) as f64 / 1_000_000.0; m.throttle_sleep_factor = self.scanner_throttle_sleep_factor_micros.load(Ordering::Relaxed) as f64 / 1_000_000.0;
m.throttle_max_sleep_seconds = self.scanner_throttle_max_sleep_millis.load(Ordering::Relaxed) as f64 / 1000.0; m.throttle_max_sleep_seconds = self.scanner_throttle_max_sleep_millis.load(Ordering::Relaxed) as f64 / 1000.0;
@@ -2081,7 +2165,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn report_tracks_successful_scan_cycle_without_failed_increment() { async fn report_tracks_successful_scan_cycle_without_failed_increment() {
let metrics = Metrics::new(); let metrics = Metrics::new();
metrics.record_scan_cycle_partial(Duration::from_millis(500), ScanCyclePartialReason::Runtime); metrics.record_scan_cycle_partial_with_source(
Duration::from_millis(500),
ScanCyclePartialReason::Runtime,
Some(ScannerWorkSource::Heal),
);
metrics.record_scan_cycle_complete(true, Duration::from_secs(2)); metrics.record_scan_cycle_complete(true, Duration::from_secs(2));
let report = metrics.report().await; let report = metrics.report().await;
@@ -2090,6 +2178,8 @@ mod tests {
assert_eq!(report.last_cycle_result_code, SCAN_CYCLE_RESULT_SUCCESS as u64); assert_eq!(report.last_cycle_result_code, SCAN_CYCLE_RESULT_SUCCESS as u64);
assert_eq!(report.last_cycle_partial_reason, SCAN_CYCLE_RESULT_UNKNOWN_LABEL); assert_eq!(report.last_cycle_partial_reason, SCAN_CYCLE_RESULT_UNKNOWN_LABEL);
assert_eq!(report.last_cycle_partial_reason_code, ScanCyclePartialReason::Unknown as u8 as u64); assert_eq!(report.last_cycle_partial_reason_code, ScanCyclePartialReason::Unknown as u8 as u64);
assert_eq!(report.last_cycle_partial_source, SCAN_CYCLE_RESULT_UNKNOWN_LABEL);
assert_eq!(report.last_cycle_partial_source_code, 0);
assert_eq!(report.last_cycle_duration_seconds, 2.0); assert_eq!(report.last_cycle_duration_seconds, 2.0);
assert_eq!(report.failed_cycles, 0); assert_eq!(report.failed_cycles, 0);
assert_eq!(report.partial_cycles, 1); assert_eq!(report.partial_cycles, 1);
@@ -2098,7 +2188,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn report_tracks_partial_scan_cycle_without_failed_increment() { async fn report_tracks_partial_scan_cycle_without_failed_increment() {
let metrics = Metrics::new(); let metrics = Metrics::new();
metrics.record_scan_cycle_partial(Duration::from_millis(2500), ScanCyclePartialReason::Directories); metrics.record_scan_cycle_partial_with_source(
Duration::from_millis(2500),
ScanCyclePartialReason::Directories,
Some(ScannerWorkSource::Usage),
);
let report = metrics.report().await; let report = metrics.report().await;
@@ -2106,6 +2200,8 @@ mod tests {
assert_eq!(report.last_cycle_result_code, SCAN_CYCLE_RESULT_PARTIAL as u64); assert_eq!(report.last_cycle_result_code, SCAN_CYCLE_RESULT_PARTIAL as u64);
assert_eq!(report.last_cycle_partial_reason, "directories"); assert_eq!(report.last_cycle_partial_reason, "directories");
assert_eq!(report.last_cycle_partial_reason_code, ScanCyclePartialReason::Directories as u8 as u64); assert_eq!(report.last_cycle_partial_reason_code, ScanCyclePartialReason::Directories as u8 as u64);
assert_eq!(report.last_cycle_partial_source, ScannerWorkSource::Usage.as_str());
assert_eq!(report.last_cycle_partial_source_code, 1);
assert_eq!(report.last_cycle_duration_seconds, 2.5); assert_eq!(report.last_cycle_duration_seconds, 2.5);
assert_eq!(report.failed_cycles, 0); assert_eq!(report.failed_cycles, 0);
assert_eq!(report.partial_cycles, 1); assert_eq!(report.partial_cycles, 1);
@@ -2113,6 +2209,12 @@ mod tests {
assert_eq!(report.partial_cycles_runtime, 0); assert_eq!(report.partial_cycles_runtime, 0);
assert_eq!(report.partial_cycles_objects, 0); assert_eq!(report.partial_cycles_objects, 0);
assert_eq!(report.partial_cycles_unknown, 0); assert_eq!(report.partial_cycles_unknown, 0);
let usage = report
.partial_cycles_by_source
.iter()
.find(|source| source.source == ScannerWorkSource::Usage.as_str())
.expect("usage partial source count should be visible");
assert_eq!(usage.cycles, 1);
} }
#[tokio::test] #[tokio::test]
+34 -1
View File
@@ -18,7 +18,8 @@ use rustfs_common::{GLOBAL_LOCAL_NODE_NAME, GLOBAL_RUSTFS_ADDR, heal_channel::Dr
use rustfs_io_metrics::internode_metrics::global_internode_metrics; use rustfs_io_metrics::internode_metrics::global_internode_metrics;
use rustfs_madmin::metrics::{ use rustfs_madmin::metrics::{
DiskIOStats, DiskMetric, LastMinute as MadminLastMinute, NetDevLine, NetMetrics, RPCMetrics, RealtimeMetrics, DiskIOStats, DiskMetric, LastMinute as MadminLastMinute, NetDevLine, NetMetrics, RPCMetrics, RealtimeMetrics,
ScannerMetrics as MadminScannerMetrics, TimedAction as MadminTimedAction, ScannerMetrics as MadminScannerMetrics, ScannerSourceCycleSnapshot as MadminScannerSourceCycleSnapshot,
TimedAction as MadminTimedAction,
}; };
use rustfs_utils::os::get_drive_stats; use rustfs_utils::os::get_drive_stats;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -99,6 +100,16 @@ fn to_madmin_scanner_metrics(metrics: rustfs_common::metrics::ScannerMetricsRepo
.collect(), .collect(),
}, },
active_paths: metrics.active_paths, active_paths: metrics.active_paths,
last_cycle_partial_source: metrics.last_cycle_partial_source,
last_cycle_partial_source_code: metrics.last_cycle_partial_source_code,
partial_cycles_by_source: metrics
.partial_cycles_by_source
.into_iter()
.map(|source| MadminScannerSourceCycleSnapshot {
source: source.source,
cycles: source.cycles,
})
.collect(),
} }
} }
@@ -343,4 +354,26 @@ mod test {
metrics.reset_for_test(); metrics.reset_for_test();
} }
#[test]
fn scanner_metrics_mapping_preserves_partial_source_status() {
let scanner = to_madmin_scanner_metrics(rustfs_common::metrics::ScannerMetricsReport {
last_cycle_partial_source: "usage".to_string(),
last_cycle_partial_source_code: 1,
partial_cycles_by_source: vec![rustfs_common::metrics::ScannerSourceCycleSnapshot {
source: "usage".to_string(),
cycles: 2,
}],
..Default::default()
});
assert_eq!(scanner.last_cycle_partial_source, "usage");
assert_eq!(scanner.last_cycle_partial_source_code, 1);
let usage = scanner
.partial_cycles_by_source
.iter()
.find(|source| source.source == "usage")
.expect("usage partial source should be mapped");
assert_eq!(usage.cycles, 2);
}
} }
+85 -1
View File
@@ -120,6 +120,14 @@ pub struct LastMinute {
pub ilm: HashMap<String, TimedAction>, pub ilm: HashMap<String, TimedAction>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScannerSourceCycleSnapshot {
#[serde(rename = "source", default)]
pub source: String,
#[serde(rename = "cycles", default)]
pub cycles: u64,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ScannerMetrics { pub struct ScannerMetrics {
#[serde(rename = "collected")] #[serde(rename = "collected")]
@@ -140,12 +148,21 @@ pub struct ScannerMetrics {
pub last_minute: LastMinute, pub last_minute: LastMinute,
#[serde(rename = "active")] #[serde(rename = "active")]
pub active_paths: Vec<String>, pub active_paths: Vec<String>,
#[serde(rename = "last_cycle_partial_source", default)]
pub last_cycle_partial_source: String,
#[serde(rename = "last_cycle_partial_source_code", default)]
pub last_cycle_partial_source_code: u64,
#[serde(rename = "partial_cycles_by_source", default)]
pub partial_cycles_by_source: Vec<ScannerSourceCycleSnapshot>,
} }
impl ScannerMetrics { impl ScannerMetrics {
pub fn merge(&mut self, other: &Self) { pub fn merge(&mut self, other: &Self) {
if self.collected_at < other.collected_at { let other_is_newer = self.collected_at < other.collected_at;
if other_is_newer {
self.collected_at = other.collected_at; self.collected_at = other.collected_at;
self.last_cycle_partial_source = other.last_cycle_partial_source.clone();
self.last_cycle_partial_source_code = other.last_cycle_partial_source_code;
} }
if self.ongoing_buckets < other.ongoing_buckets { if self.ongoing_buckets < other.ongoing_buckets {
@@ -182,6 +199,20 @@ impl ScannerMetrics {
self.last_minute.ilm.entry(k.clone()).or_default().merge(v); self.last_minute.ilm.entry(k.clone()).or_default().merge(v);
} }
for source in other.partial_cycles_by_source.iter() {
if let Some(existing) = self
.partial_cycles_by_source
.iter_mut()
.find(|existing| existing.source == source.source)
{
existing.cycles += source.cycles;
} else {
self.partial_cycles_by_source.push(source.clone());
}
}
self.partial_cycles_by_source
.sort_by(|left, right| left.source.cmp(&right.source));
self.active_paths.extend(other.active_paths.clone()); self.active_paths.extend(other.active_paths.clone());
self.active_paths.sort(); self.active_paths.sort();
@@ -668,3 +699,56 @@ pub struct Operations {
#[serde(rename = "operations")] #[serde(rename = "operations")]
pub operations: HashMap<String, TimedAction>, pub operations: HashMap<String, TimedAction>,
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scanner_metrics_merge_aggregates_partial_cycles_by_source() {
let collected_at = Utc::now();
let mut scanner = ScannerMetrics {
collected_at,
last_cycle_partial_source: "usage".to_string(),
last_cycle_partial_source_code: 1,
partial_cycles_by_source: vec![ScannerSourceCycleSnapshot {
source: "usage".to_string(),
cycles: 1,
}],
..Default::default()
};
scanner.merge(&ScannerMetrics {
collected_at: collected_at + chrono::Duration::seconds(1),
last_cycle_partial_source: "lifecycle".to_string(),
last_cycle_partial_source_code: 2,
partial_cycles_by_source: vec![
ScannerSourceCycleSnapshot {
source: "usage".to_string(),
cycles: 2,
},
ScannerSourceCycleSnapshot {
source: "lifecycle".to_string(),
cycles: 1,
},
],
..Default::default()
});
assert_eq!(scanner.last_cycle_partial_source, "lifecycle");
assert_eq!(scanner.last_cycle_partial_source_code, 2);
assert_eq!(
scanner.partial_cycles_by_source,
vec![
ScannerSourceCycleSnapshot {
source: "lifecycle".to_string(),
cycles: 1,
},
ScannerSourceCycleSnapshot {
source: "usage".to_string(),
cycles: 3,
},
]
);
}
}
+35 -3
View File
@@ -27,7 +27,8 @@ use crate::{DataUsageInfo, ScannerActivityGuard, ScannerError};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use rustfs_common::heal_channel::HealScanMode; use rustfs_common::heal_channel::HealScanMode;
use rustfs_common::metrics::{ use rustfs_common::metrics::{
CurrentCycle, Metric, Metrics, ScanCyclePartialReason, emit_scan_cycle_complete, emit_scan_cycle_partial, global_metrics, CurrentCycle, Metric, Metrics, ScanCyclePartialReason, ScannerWorkSource, emit_scan_cycle_complete,
emit_scan_cycle_partial_with_source, global_metrics,
}; };
use rustfs_config::ScannerSpeed; use rustfs_config::ScannerSpeed;
#[cfg(test)] #[cfg(test)]
@@ -96,6 +97,13 @@ fn scan_cycle_partial_reason(reason: Option<ScannerCycleBudgetReason>) -> ScanCy
} }
} }
fn scan_cycle_partial_source(reason: Option<ScannerCycleBudgetReason>) -> Option<ScannerWorkSource> {
match reason {
Some(ScannerCycleBudgetReason::Objects | ScannerCycleBudgetReason::Directories) => Some(ScannerWorkSource::Usage),
Some(ScannerCycleBudgetReason::Runtime) | None => None,
}
}
/// Compute a randomized inter-cycle sleep. /// Compute a randomized inter-cycle sleep.
// Delay is scan interval +- 10%, with a floor of 1 second. // Delay is scan interval +- 10%, with a floor of 1 second.
fn randomized_cycle_delay() -> Duration { fn randomized_cycle_delay() -> Duration {
@@ -505,7 +513,12 @@ async fn run_data_scanner_cycle(ctx: &CancellationToken, storeapi: &Arc<ECStore>
max_directories = ?cycle_budget.max_directories(), max_directories = ?cycle_budget.max_directories(),
"Data scanner cycle stopped after reaching its cycle budget" "Data scanner cycle stopped after reaching its cycle budget"
); );
emit_scan_cycle_partial(cycle_start.elapsed(), scan_cycle_partial_reason(cycle_budget.reason())); let budget_reason = cycle_budget.reason();
emit_scan_cycle_partial_with_source(
cycle_start.elapsed(),
scan_cycle_partial_reason(budget_reason),
scan_cycle_partial_source(budget_reason),
);
mark_scan_cycle_idle(cycle_info).await; mark_scan_cycle_idle(cycle_info).await;
return; return;
} }
@@ -526,7 +539,12 @@ async fn run_data_scanner_cycle(ctx: &CancellationToken, storeapi: &Arc<ECStore>
"Data scanner cycle stopped after reaching its cycle budget" "Data scanner cycle stopped after reaching its cycle budget"
); );
global_metrics().finish_scan_cycle_work(cycle_work_start); global_metrics().finish_scan_cycle_work(cycle_work_start);
emit_scan_cycle_partial(cycle_start.elapsed(), scan_cycle_partial_reason(cycle_budget.reason())); let budget_reason = cycle_budget.reason();
emit_scan_cycle_partial_with_source(
cycle_start.elapsed(),
scan_cycle_partial_reason(budget_reason),
scan_cycle_partial_source(budget_reason),
);
mark_scan_cycle_idle(cycle_info).await; mark_scan_cycle_idle(cycle_info).await;
return; return;
} }
@@ -855,6 +873,20 @@ mod tests {
assert_eq!(scan_cycle_partial_reason(None), ScanCyclePartialReason::Unknown); assert_eq!(scan_cycle_partial_reason(None), ScanCyclePartialReason::Unknown);
} }
#[test]
fn test_scan_cycle_partial_source_maps_budget_reason() {
assert_eq!(scan_cycle_partial_source(Some(ScannerCycleBudgetReason::Runtime)), None);
assert_eq!(
scan_cycle_partial_source(Some(ScannerCycleBudgetReason::Objects)),
Some(ScannerWorkSource::Usage)
);
assert_eq!(
scan_cycle_partial_source(Some(ScannerCycleBudgetReason::Directories)),
Some(ScannerWorkSource::Usage)
);
assert_eq!(scan_cycle_partial_source(None), None);
}
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_mark_scan_cycle_idle_clears_published_cycle_state() { async fn test_mark_scan_cycle_idle_clears_published_cycle_state() {