mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
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:
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user