mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(scanner): add source-aware maintenance controls (#3461)
* feat(scanner): add source-aware maintenance controls * fix(scanner): preserve source control between cycles --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
This commit is contained in:
@@ -762,6 +762,34 @@ pub struct ScannerSourceWorkSnapshot {
|
|||||||
pub missed: u64,
|
pub missed: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ScannerMaintenanceSourceSnapshot {
|
||||||
|
pub source: String,
|
||||||
|
pub state: String,
|
||||||
|
pub reason: String,
|
||||||
|
pub backlog: u64,
|
||||||
|
pub current_checked: u64,
|
||||||
|
pub current_queued: u64,
|
||||||
|
pub current_missed: u64,
|
||||||
|
pub lifetime_missed: u64,
|
||||||
|
pub partial_cycles: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ScannerMaintenanceControlSnapshot {
|
||||||
|
pub primary_control: String,
|
||||||
|
pub sources: Vec<ScannerMaintenanceSourceSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScannerMaintenanceControlSnapshot {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
primary_control: SCANNER_MAINTENANCE_CONTROL_NONE.to_string(),
|
||||||
|
sources: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct ScannerSourceCycleSnapshot {
|
pub struct ScannerSourceCycleSnapshot {
|
||||||
pub source: String,
|
pub source: String,
|
||||||
@@ -940,6 +968,8 @@ pub struct ScannerMetricsReport {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lifecycle_transition: ScannerLifecycleTransitionSnapshot,
|
pub lifecycle_transition: ScannerLifecycleTransitionSnapshot,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub maintenance_control: ScannerMaintenanceControlSnapshot,
|
||||||
|
#[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,
|
||||||
@@ -1090,6 +1120,164 @@ fn scanner_pacing_pressure(metrics: &ScannerMetricsReport) -> ScannerPacingPress
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_NONE: &str = "none";
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_BLOCKED_SOURCE: &str = "blocked_source";
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_DEFERRED_SOURCE: &str = "deferred_source";
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_ACTIVE_SOURCE: &str = "active_source";
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_PACING_PRESSURE: &str = "pacing_pressure";
|
||||||
|
|
||||||
|
const SCANNER_MAINTENANCE_STATE_IDLE: &str = "idle";
|
||||||
|
const SCANNER_MAINTENANCE_STATE_ACTIVE: &str = "active";
|
||||||
|
const SCANNER_MAINTENANCE_STATE_DEFERRED: &str = "deferred";
|
||||||
|
const SCANNER_MAINTENANCE_STATE_BLOCKED: &str = "blocked";
|
||||||
|
|
||||||
|
const SCANNER_MAINTENANCE_REASON_IDLE: &str = "idle";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_ACTIVE_WORK: &str = "active_work";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_QUEUED_WORK: &str = "queued_work";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_PARTIAL_CYCLE: &str = "partial_cycle";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_MISSED_WORK: &str = "missed_work";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_TRANSITION_QUEUE_BACKLOG: &str = "transition_queue_backlog";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_TRANSITION_QUEUE_FULL: &str = "transition_queue_full";
|
||||||
|
|
||||||
|
fn scanner_source_work_snapshot(work: &[ScannerSourceWorkSnapshot], source: ScannerWorkSource) -> ScannerSourceWorkSnapshot {
|
||||||
|
work.iter()
|
||||||
|
.find(|snapshot| snapshot.source == source.as_str())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| ScannerSourceWorkSnapshot {
|
||||||
|
source: source.as_str().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_source_partial_cycles(metrics: &ScannerMetricsReport, source: ScannerWorkSource) -> u64 {
|
||||||
|
metrics
|
||||||
|
.partial_cycles_by_source
|
||||||
|
.iter()
|
||||||
|
.find(|snapshot| snapshot.source == source.as_str())
|
||||||
|
.map(|snapshot| snapshot.cycles)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_source_has_current_work(work: &ScannerSourceWorkSnapshot) -> bool {
|
||||||
|
work.checked > 0 || work.queued > 0 || work.executed > 0 || work.failed > 0 || work.skipped > 0 || work.missed > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_lifecycle_transition_backlog(metrics: &ScannerMetricsReport) -> u64 {
|
||||||
|
metrics
|
||||||
|
.lifecycle_transition
|
||||||
|
.current_queued
|
||||||
|
.saturating_add(metrics.lifecycle_transition.current_active)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_source_maintenance_state(
|
||||||
|
source: ScannerWorkSource,
|
||||||
|
current: &ScannerSourceWorkSnapshot,
|
||||||
|
metrics: &ScannerMetricsReport,
|
||||||
|
) -> (&'static str, &'static str, u64) {
|
||||||
|
if current.missed > 0 {
|
||||||
|
return (SCANNER_MAINTENANCE_STATE_BLOCKED, SCANNER_MAINTENANCE_REASON_MISSED_WORK, current.missed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if source == ScannerWorkSource::Lifecycle {
|
||||||
|
let transition_backlog = scanner_lifecycle_transition_backlog(metrics);
|
||||||
|
if metrics.lifecycle_transition.current_queue_capacity > 0
|
||||||
|
&& metrics.lifecycle_transition.current_queued >= metrics.lifecycle_transition.current_queue_capacity
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
SCANNER_MAINTENANCE_STATE_BLOCKED,
|
||||||
|
SCANNER_MAINTENANCE_REASON_TRANSITION_QUEUE_FULL,
|
||||||
|
transition_backlog,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if metrics.last_cycle_partial_source == source.as_str() && metrics.pacing_pressure.last_cycle_budget_limited {
|
||||||
|
return (SCANNER_MAINTENANCE_STATE_DEFERRED, SCANNER_MAINTENANCE_REASON_PARTIAL_CYCLE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.queued > 0 {
|
||||||
|
return (SCANNER_MAINTENANCE_STATE_ACTIVE, SCANNER_MAINTENANCE_REASON_QUEUED_WORK, current.queued);
|
||||||
|
}
|
||||||
|
|
||||||
|
if source == ScannerWorkSource::Lifecycle {
|
||||||
|
let transition_backlog = scanner_lifecycle_transition_backlog(metrics);
|
||||||
|
if transition_backlog > 0 {
|
||||||
|
return (
|
||||||
|
SCANNER_MAINTENANCE_STATE_ACTIVE,
|
||||||
|
SCANNER_MAINTENANCE_REASON_TRANSITION_QUEUE_BACKLOG,
|
||||||
|
transition_backlog,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner_source_has_current_work(current) {
|
||||||
|
return (SCANNER_MAINTENANCE_STATE_ACTIVE, SCANNER_MAINTENANCE_REASON_ACTIVE_WORK, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
(SCANNER_MAINTENANCE_STATE_IDLE, SCANNER_MAINTENANCE_REASON_IDLE, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_maintenance_source_work(metrics: &ScannerMetricsReport) -> &[ScannerSourceWorkSnapshot] {
|
||||||
|
if metrics.current_cycle_source_work.is_empty() {
|
||||||
|
&metrics.last_cycle_source_work
|
||||||
|
} else {
|
||||||
|
&metrics.current_cycle_source_work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_maintenance_control(metrics: &ScannerMetricsReport) -> ScannerMaintenanceControlSnapshot {
|
||||||
|
let mut has_blocked = false;
|
||||||
|
let mut has_deferred = false;
|
||||||
|
let mut has_active = false;
|
||||||
|
let current_source_work = scanner_maintenance_source_work(metrics);
|
||||||
|
|
||||||
|
let sources = ScannerWorkSource::all()
|
||||||
|
.iter()
|
||||||
|
.map(|source| {
|
||||||
|
let current = scanner_source_work_snapshot(current_source_work, *source);
|
||||||
|
let lifetime = scanner_source_work_snapshot(&metrics.source_work, *source);
|
||||||
|
let partial_cycles = scanner_source_partial_cycles(metrics, *source);
|
||||||
|
let (state, reason, backlog) = scanner_source_maintenance_state(*source, ¤t, metrics);
|
||||||
|
|
||||||
|
match state {
|
||||||
|
SCANNER_MAINTENANCE_STATE_BLOCKED => has_blocked = true,
|
||||||
|
SCANNER_MAINTENANCE_STATE_DEFERRED => has_deferred = true,
|
||||||
|
SCANNER_MAINTENANCE_STATE_ACTIVE => has_active = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScannerMaintenanceSourceSnapshot {
|
||||||
|
source: source.as_str().to_string(),
|
||||||
|
state: state.to_string(),
|
||||||
|
reason: reason.to_string(),
|
||||||
|
backlog,
|
||||||
|
current_checked: current.checked,
|
||||||
|
current_queued: current.queued,
|
||||||
|
current_missed: current.missed,
|
||||||
|
lifetime_missed: lifetime.missed,
|
||||||
|
partial_cycles,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let primary_control = if has_blocked {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_BLOCKED_SOURCE
|
||||||
|
} else if has_deferred {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_DEFERRED_SOURCE
|
||||||
|
} else if has_active {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_ACTIVE_SOURCE
|
||||||
|
} else if metrics.pacing_pressure.primary_pressure != SCANNER_MAINTENANCE_CONTROL_NONE {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_PACING_PRESSURE
|
||||||
|
} else {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
ScannerMaintenanceControlSnapshot {
|
||||||
|
primary_control: primary_control.to_string(),
|
||||||
|
sources,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn duration_millis_saturated(duration: Duration) -> u64 {
|
fn duration_millis_saturated(duration: Duration) -> u64 {
|
||||||
duration.as_millis().min(u64::MAX as u128) as u64
|
duration.as_millis().min(u64::MAX as u128) as u64
|
||||||
}
|
}
|
||||||
@@ -2163,6 +2351,7 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.pacing_pressure = scanner_pacing_pressure(&m);
|
m.pacing_pressure = scanner_pacing_pressure(&m);
|
||||||
|
m.maintenance_control = scanner_maintenance_control(&m);
|
||||||
|
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
@@ -2479,6 +2668,86 @@ mod tests {
|
|||||||
assert_eq!(report.lifecycle_transition.failed, 1);
|
assert_eq!(report.lifecycle_transition.failed, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn report_derives_scanner_maintenance_control_status() {
|
||||||
|
let metrics = Metrics::new();
|
||||||
|
let start = metrics.start_scan_cycle_work();
|
||||||
|
metrics.record_scanner_source_checked(ScannerWorkSource::Usage, 3);
|
||||||
|
metrics.record_scanner_source_missed(ScannerWorkSource::Lifecycle, 2);
|
||||||
|
metrics.record_scanner_source_queued(ScannerWorkSource::BucketReplication, 1);
|
||||||
|
metrics.record_scan_cycle_partial_with_source(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
ScanCyclePartialReason::Objects,
|
||||||
|
Some(ScannerWorkSource::Usage),
|
||||||
|
);
|
||||||
|
metrics.record_scanner_lifecycle_transition_state(ScannerLifecycleTransitionStateUpdate {
|
||||||
|
queue_capacity: 4,
|
||||||
|
queued: 2,
|
||||||
|
queue_full: 1,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let report = metrics.report().await;
|
||||||
|
|
||||||
|
assert_eq!(report.maintenance_control.primary_control, "blocked_source");
|
||||||
|
let usage = report
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == ScannerWorkSource::Usage.as_str())
|
||||||
|
.expect("usage source control should be visible");
|
||||||
|
assert_eq!(usage.state, "deferred");
|
||||||
|
assert_eq!(usage.reason, "partial_cycle");
|
||||||
|
assert_eq!(usage.current_checked, 3);
|
||||||
|
assert_eq!(usage.partial_cycles, 1);
|
||||||
|
|
||||||
|
let lifecycle = report
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == ScannerWorkSource::Lifecycle.as_str())
|
||||||
|
.expect("lifecycle source control should be visible");
|
||||||
|
assert_eq!(lifecycle.state, "blocked");
|
||||||
|
assert_eq!(lifecycle.reason, "missed_work");
|
||||||
|
assert_eq!(lifecycle.current_missed, 2);
|
||||||
|
assert_eq!(lifecycle.backlog, 2);
|
||||||
|
|
||||||
|
let bucket_replication = report
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == ScannerWorkSource::BucketReplication.as_str())
|
||||||
|
.expect("bucket replication source control should be visible");
|
||||||
|
assert_eq!(bucket_replication.state, "active");
|
||||||
|
assert_eq!(bucket_replication.reason, "queued_work");
|
||||||
|
assert_eq!(bucket_replication.current_queued, 1);
|
||||||
|
|
||||||
|
metrics.finish_scan_cycle_work(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn report_uses_last_cycle_source_work_for_maintenance_control_between_cycles() {
|
||||||
|
let metrics = Metrics::new();
|
||||||
|
let start = metrics.start_scan_cycle_work();
|
||||||
|
metrics.record_scanner_source_missed(ScannerWorkSource::Lifecycle, 2);
|
||||||
|
|
||||||
|
metrics.finish_scan_cycle_work(start);
|
||||||
|
let report = metrics.report().await;
|
||||||
|
|
||||||
|
assert!(report.current_cycle_source_work.is_empty());
|
||||||
|
let lifecycle = report
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == ScannerWorkSource::Lifecycle.as_str())
|
||||||
|
.expect("lifecycle source control should be visible");
|
||||||
|
assert_eq!(report.maintenance_control.primary_control, "blocked_source");
|
||||||
|
assert_eq!(lifecycle.state, "blocked");
|
||||||
|
assert_eq!(lifecycle.reason, "missed_work");
|
||||||
|
assert_eq!(lifecycle.current_missed, 2);
|
||||||
|
assert_eq!(lifecycle.backlog, 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn scanner_metrics_update_source_work() {
|
async fn scanner_metrics_update_source_work() {
|
||||||
let metrics = Metrics::new();
|
let metrics = Metrics::new();
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ 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,
|
||||||
ScannerCheckpointReport as MadminScannerCheckpointReport,
|
ScannerCheckpointReport as MadminScannerCheckpointReport,
|
||||||
ScannerLifecycleTransitionSnapshot as MadminScannerLifecycleTransitionSnapshot, ScannerMetrics as MadminScannerMetrics,
|
ScannerLifecycleTransitionSnapshot as MadminScannerLifecycleTransitionSnapshot,
|
||||||
|
ScannerMaintenanceControlSnapshot as MadminScannerMaintenanceControlSnapshot,
|
||||||
|
ScannerMaintenanceSourceSnapshot as MadminScannerMaintenanceSourceSnapshot, ScannerMetrics as MadminScannerMetrics,
|
||||||
ScannerPacingPressureSnapshot as MadminScannerPacingPressureSnapshot,
|
ScannerPacingPressureSnapshot as MadminScannerPacingPressureSnapshot,
|
||||||
ScannerSourceCycleSnapshot as MadminScannerSourceCycleSnapshot, ScannerSourceWorkSnapshot as MadminScannerSourceWorkSnapshot,
|
ScannerSourceCycleSnapshot as MadminScannerSourceCycleSnapshot, ScannerSourceWorkSnapshot as MadminScannerSourceWorkSnapshot,
|
||||||
TimedAction as MadminTimedAction,
|
TimedAction as MadminTimedAction,
|
||||||
@@ -177,6 +179,25 @@ fn to_madmin_scanner_metrics(metrics: rustfs_common::metrics::ScannerMetricsRepo
|
|||||||
completed: metrics.lifecycle_transition.completed,
|
completed: metrics.lifecycle_transition.completed,
|
||||||
failed: metrics.lifecycle_transition.failed,
|
failed: metrics.lifecycle_transition.failed,
|
||||||
},
|
},
|
||||||
|
maintenance_control: MadminScannerMaintenanceControlSnapshot {
|
||||||
|
primary_control: metrics.maintenance_control.primary_control,
|
||||||
|
sources: metrics
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.into_iter()
|
||||||
|
.map(|source| MadminScannerMaintenanceSourceSnapshot {
|
||||||
|
source: source.source,
|
||||||
|
state: source.state,
|
||||||
|
reason: source.reason,
|
||||||
|
backlog: source.backlog,
|
||||||
|
current_checked: source.current_checked,
|
||||||
|
current_queued: source.current_queued,
|
||||||
|
current_missed: source.current_missed,
|
||||||
|
lifetime_missed: source.lifetime_missed,
|
||||||
|
partial_cycles: source.partial_cycles,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
partial_cycles_by_source: metrics
|
partial_cycles_by_source: metrics
|
||||||
.partial_cycles_by_source
|
.partial_cycles_by_source
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -580,6 +601,43 @@ mod test {
|
|||||||
assert_eq!(scanner.last_cycle_lifecycle_transition_actions, 7);
|
assert_eq!(scanner.last_cycle_lifecycle_transition_actions, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scanner_metrics_mapping_preserves_maintenance_control_status() {
|
||||||
|
let scanner = to_madmin_scanner_metrics(rustfs_common::metrics::ScannerMetricsReport {
|
||||||
|
maintenance_control: rustfs_common::metrics::ScannerMaintenanceControlSnapshot {
|
||||||
|
primary_control: "blocked_source".to_string(),
|
||||||
|
sources: vec![rustfs_common::metrics::ScannerMaintenanceSourceSnapshot {
|
||||||
|
source: "lifecycle".to_string(),
|
||||||
|
state: "blocked".to_string(),
|
||||||
|
reason: "missed_work".to_string(),
|
||||||
|
backlog: 4,
|
||||||
|
current_checked: 2,
|
||||||
|
current_queued: 1,
|
||||||
|
current_missed: 3,
|
||||||
|
lifetime_missed: 8,
|
||||||
|
partial_cycles: 5,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(scanner.maintenance_control.primary_control, "blocked_source");
|
||||||
|
let lifecycle = scanner
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == "lifecycle")
|
||||||
|
.expect("lifecycle maintenance control should be mapped");
|
||||||
|
assert_eq!(lifecycle.state, "blocked");
|
||||||
|
assert_eq!(lifecycle.reason, "missed_work");
|
||||||
|
assert_eq!(lifecycle.backlog, 4);
|
||||||
|
assert_eq!(lifecycle.current_checked, 2);
|
||||||
|
assert_eq!(lifecycle.current_queued, 1);
|
||||||
|
assert_eq!(lifecycle.current_missed, 3);
|
||||||
|
assert_eq!(lifecycle.lifetime_missed, 8);
|
||||||
|
assert_eq!(lifecycle.partial_cycles, 5);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scanner_metrics_mapping_preserves_distributed_status_fields() {
|
fn scanner_metrics_mapping_preserves_distributed_status_fields() {
|
||||||
let scanner = to_madmin_scanner_metrics(rustfs_common::metrics::ScannerMetricsReport {
|
let scanner = to_madmin_scanner_metrics(rustfs_common::metrics::ScannerMetricsReport {
|
||||||
|
|||||||
@@ -158,6 +158,108 @@ pub struct ScannerSourceWorkSnapshot {
|
|||||||
pub missed: u64,
|
pub missed: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ScannerMaintenanceSourceSnapshot {
|
||||||
|
#[serde(rename = "source", default)]
|
||||||
|
pub source: String,
|
||||||
|
#[serde(rename = "state", default)]
|
||||||
|
pub state: String,
|
||||||
|
#[serde(rename = "reason", default)]
|
||||||
|
pub reason: String,
|
||||||
|
#[serde(rename = "backlog", default)]
|
||||||
|
pub backlog: u64,
|
||||||
|
#[serde(rename = "current_checked", default)]
|
||||||
|
pub current_checked: u64,
|
||||||
|
#[serde(rename = "current_queued", default)]
|
||||||
|
pub current_queued: u64,
|
||||||
|
#[serde(rename = "current_missed", default)]
|
||||||
|
pub current_missed: u64,
|
||||||
|
#[serde(rename = "lifetime_missed", default)]
|
||||||
|
pub lifetime_missed: u64,
|
||||||
|
#[serde(rename = "partial_cycles", default)]
|
||||||
|
pub partial_cycles: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScannerMaintenanceSourceSnapshot {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
if scanner_maintenance_state_priority(&other.state) > scanner_maintenance_state_priority(&self.state) {
|
||||||
|
self.state = other.state.clone();
|
||||||
|
self.reason = other.reason.clone();
|
||||||
|
} else if self.reason.is_empty() || self.reason == SCANNER_MAINTENANCE_REASON_IDLE {
|
||||||
|
self.reason = other.reason.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backlog = self.backlog.saturating_add(other.backlog);
|
||||||
|
self.current_checked = self.current_checked.saturating_add(other.current_checked);
|
||||||
|
self.current_queued = self.current_queued.saturating_add(other.current_queued);
|
||||||
|
self.current_missed = self.current_missed.saturating_add(other.current_missed);
|
||||||
|
self.lifetime_missed = self.lifetime_missed.saturating_add(other.lifetime_missed);
|
||||||
|
self.partial_cycles = self.partial_cycles.saturating_add(other.partial_cycles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCANNER_MAINTENANCE_CONTROL_NONE: &str = "none";
|
||||||
|
const SCANNER_MAINTENANCE_REASON_IDLE: &str = "idle";
|
||||||
|
|
||||||
|
fn scanner_maintenance_state_priority(state: &str) -> u8 {
|
||||||
|
match state {
|
||||||
|
"blocked" => 3,
|
||||||
|
"deferred" => 2,
|
||||||
|
"active" => 1,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanner_maintenance_control_priority(control: &str) -> u8 {
|
||||||
|
match control {
|
||||||
|
"blocked_source" => 4,
|
||||||
|
"deferred_source" => 3,
|
||||||
|
"active_source" => 2,
|
||||||
|
"pacing_pressure" => 1,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_scanner_maintenance_control() -> String {
|
||||||
|
SCANNER_MAINTENANCE_CONTROL_NONE.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ScannerMaintenanceControlSnapshot {
|
||||||
|
#[serde(rename = "primary_control", default = "default_scanner_maintenance_control")]
|
||||||
|
pub primary_control: String,
|
||||||
|
#[serde(rename = "sources", default)]
|
||||||
|
pub sources: Vec<ScannerMaintenanceSourceSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScannerMaintenanceControlSnapshot {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
primary_control: default_scanner_maintenance_control(),
|
||||||
|
sources: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScannerMaintenanceControlSnapshot {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
if scanner_maintenance_control_priority(&other.primary_control)
|
||||||
|
> scanner_maintenance_control_priority(&self.primary_control)
|
||||||
|
{
|
||||||
|
self.primary_control = other.primary_control.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
for source in other.sources.iter() {
|
||||||
|
if let Some(existing) = self.sources.iter_mut().find(|existing| existing.source == source.source) {
|
||||||
|
existing.merge(source);
|
||||||
|
} else {
|
||||||
|
self.sources.push(source.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sources.sort_by(|left, right| left.source.cmp(&right.source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ScannerSourceWorkSnapshot {
|
impl ScannerSourceWorkSnapshot {
|
||||||
fn merge(&mut self, other: &Self) {
|
fn merge(&mut self, other: &Self) {
|
||||||
self.checked = self.checked.saturating_add(other.checked);
|
self.checked = self.checked.saturating_add(other.checked);
|
||||||
@@ -424,6 +526,8 @@ pub struct ScannerMetrics {
|
|||||||
pub pacing_pressure: ScannerPacingPressureSnapshot,
|
pub pacing_pressure: ScannerPacingPressureSnapshot,
|
||||||
#[serde(rename = "lifecycle_transition", default)]
|
#[serde(rename = "lifecycle_transition", default)]
|
||||||
pub lifecycle_transition: ScannerLifecycleTransitionSnapshot,
|
pub lifecycle_transition: ScannerLifecycleTransitionSnapshot,
|
||||||
|
#[serde(rename = "maintenance_control", default)]
|
||||||
|
pub maintenance_control: ScannerMaintenanceControlSnapshot,
|
||||||
#[serde(rename = "throttle_idle_mode_enabled", default)]
|
#[serde(rename = "throttle_idle_mode_enabled", default)]
|
||||||
pub throttle_idle_mode_enabled: bool,
|
pub throttle_idle_mode_enabled: bool,
|
||||||
#[serde(rename = "throttle_sleep_factor", default)]
|
#[serde(rename = "throttle_sleep_factor", default)]
|
||||||
@@ -496,6 +600,7 @@ impl ScannerMetrics {
|
|||||||
self.oldest_active_path_age_seconds = self.oldest_active_path_age_seconds.max(other.oldest_active_path_age_seconds);
|
self.oldest_active_path_age_seconds = self.oldest_active_path_age_seconds.max(other.oldest_active_path_age_seconds);
|
||||||
self.pacing_pressure.merge(&other.pacing_pressure);
|
self.pacing_pressure.merge(&other.pacing_pressure);
|
||||||
self.lifecycle_transition.merge(&other.lifecycle_transition);
|
self.lifecycle_transition.merge(&other.lifecycle_transition);
|
||||||
|
self.maintenance_control.merge(&other.maintenance_control);
|
||||||
self.current_set_scan_concurrency_limit = self
|
self.current_set_scan_concurrency_limit = self
|
||||||
.current_set_scan_concurrency_limit
|
.current_set_scan_concurrency_limit
|
||||||
.saturating_add(other.current_set_scan_concurrency_limit);
|
.saturating_add(other.current_set_scan_concurrency_limit);
|
||||||
@@ -1307,6 +1412,87 @@ mod tests {
|
|||||||
assert_eq!(scanner.last_cycle_lifecycle_transition_actions, 26);
|
assert_eq!(scanner.last_cycle_lifecycle_transition_actions, 26);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scanner_metrics_merge_aggregates_maintenance_control_status() {
|
||||||
|
let collected_at = Utc::now();
|
||||||
|
let mut scanner = ScannerMetrics {
|
||||||
|
collected_at,
|
||||||
|
maintenance_control: ScannerMaintenanceControlSnapshot {
|
||||||
|
primary_control: "active_source".to_string(),
|
||||||
|
sources: vec![ScannerMaintenanceSourceSnapshot {
|
||||||
|
source: "usage".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
reason: "active_work".to_string(),
|
||||||
|
backlog: 1,
|
||||||
|
current_checked: 2,
|
||||||
|
current_queued: 0,
|
||||||
|
current_missed: 0,
|
||||||
|
lifetime_missed: 0,
|
||||||
|
partial_cycles: 0,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
scanner.merge(&ScannerMetrics {
|
||||||
|
collected_at: collected_at + chrono::Duration::seconds(1),
|
||||||
|
maintenance_control: ScannerMaintenanceControlSnapshot {
|
||||||
|
primary_control: "blocked_source".to_string(),
|
||||||
|
sources: vec![
|
||||||
|
ScannerMaintenanceSourceSnapshot {
|
||||||
|
source: "usage".to_string(),
|
||||||
|
state: "deferred".to_string(),
|
||||||
|
reason: "partial_cycle".to_string(),
|
||||||
|
backlog: 3,
|
||||||
|
current_checked: 5,
|
||||||
|
current_queued: 1,
|
||||||
|
current_missed: 0,
|
||||||
|
lifetime_missed: 0,
|
||||||
|
partial_cycles: 2,
|
||||||
|
},
|
||||||
|
ScannerMaintenanceSourceSnapshot {
|
||||||
|
source: "lifecycle".to_string(),
|
||||||
|
state: "blocked".to_string(),
|
||||||
|
reason: "missed_work".to_string(),
|
||||||
|
backlog: 4,
|
||||||
|
current_checked: 0,
|
||||||
|
current_queued: 0,
|
||||||
|
current_missed: 4,
|
||||||
|
lifetime_missed: 9,
|
||||||
|
partial_cycles: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(scanner.maintenance_control.primary_control, "blocked_source");
|
||||||
|
let lifecycle = scanner
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == "lifecycle")
|
||||||
|
.expect("lifecycle maintenance control should be present");
|
||||||
|
assert_eq!(lifecycle.state, "blocked");
|
||||||
|
assert_eq!(lifecycle.reason, "missed_work");
|
||||||
|
assert_eq!(lifecycle.backlog, 4);
|
||||||
|
assert_eq!(lifecycle.current_missed, 4);
|
||||||
|
assert_eq!(lifecycle.lifetime_missed, 9);
|
||||||
|
|
||||||
|
let usage = scanner
|
||||||
|
.maintenance_control
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.find(|source| source.source == "usage")
|
||||||
|
.expect("usage maintenance control should be present");
|
||||||
|
assert_eq!(usage.state, "deferred");
|
||||||
|
assert_eq!(usage.reason, "partial_cycle");
|
||||||
|
assert_eq!(usage.backlog, 4);
|
||||||
|
assert_eq!(usage.current_checked, 7);
|
||||||
|
assert_eq!(usage.current_queued, 1);
|
||||||
|
assert_eq!(usage.partial_cycles, 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scanner_metrics_merge_preserves_distributed_status_fields() {
|
fn scanner_metrics_merge_preserves_distributed_status_fields() {
|
||||||
let collected_at = Utc::now();
|
let collected_at = Utc::now();
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ Compare these fields between baseline and tuned runs:
|
|||||||
| `runtime_config.*.value` and `runtime_config.*.source` | Confirms the tested settings actually took effect. |
|
| `runtime_config.*.value` and `runtime_config.*.source` | Confirms the tested settings actually took effect. |
|
||||||
| `metrics.pacing_pressure.primary_pressure` | Shows whether pressure is from queues, budgets, pause activity, active scans, or no scanner pressure. |
|
| `metrics.pacing_pressure.primary_pressure` | Shows whether pressure is from queues, budgets, pause activity, active scans, or no scanner pressure. |
|
||||||
| `metrics.pacing_pressure.last_cycle_total_pause_ratio` | Shows how much of the last cycle was cooperative scanner pause time. |
|
| `metrics.pacing_pressure.last_cycle_total_pause_ratio` | Shows how much of the last cycle was cooperative scanner pause time. |
|
||||||
|
| `metrics.maintenance_control.primary_control` | Shows whether source-level maintenance is blocked, deferred, active, only pacing-limited, or idle. |
|
||||||
|
| `metrics.maintenance_control.sources` | Shows the source, state, reason, backlog, current or last-cycle missed work, and partial-cycle count for each scanner maintenance source. |
|
||||||
| `metrics.current_cycle_objects_scanned` | Confirms object scan progress during the current cycle. |
|
| `metrics.current_cycle_objects_scanned` | Confirms object scan progress during the current cycle. |
|
||||||
| `metrics.current_cycle_directories_scanned` | Confirms directory walk progress during the current cycle. |
|
| `metrics.current_cycle_directories_scanned` | Confirms directory walk progress during the current cycle. |
|
||||||
| `metrics.last_cycle_result` | Confirms whether the previous cycle completed, stopped partially, or failed. |
|
| `metrics.last_cycle_result` | Confirms whether the previous cycle completed, stopped partially, or failed. |
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ metrics.pacing_pressure.primary_pressure
|
|||||||
metrics.pacing_pressure.last_cycle_budget_limited
|
metrics.pacing_pressure.last_cycle_budget_limited
|
||||||
metrics.lifecycle_transition.current_queued
|
metrics.lifecycle_transition.current_queued
|
||||||
metrics.lifecycle_transition.scanner_missed
|
metrics.lifecycle_transition.scanner_missed
|
||||||
|
metrics.maintenance_control.primary_control
|
||||||
metrics.source_work
|
metrics.source_work
|
||||||
metrics.scan_checkpoint
|
metrics.scan_checkpoint
|
||||||
```
|
```
|
||||||
@@ -146,6 +147,45 @@ Use these counters to decide whether scan progress is limited by scanner pacing
|
|||||||
or by a downstream subsystem such as lifecycle transition, replication repair,
|
or by a downstream subsystem such as lifecycle transition, replication repair,
|
||||||
or heal admission.
|
or heal admission.
|
||||||
|
|
||||||
|
## Reading Maintenance Control
|
||||||
|
|
||||||
|
`metrics.maintenance_control` derives a source-level control snapshot from
|
||||||
|
scanner pacing, partial-cycle state, source work, and lifecycle transition
|
||||||
|
queue state. It does not change scanner scheduling by itself; it explains why a
|
||||||
|
source is moving, deferred, or blocked. When no scan cycle is currently active,
|
||||||
|
source-work controls use the last completed cycle so recently missed work stays
|
||||||
|
visible between scanner passes.
|
||||||
|
|
||||||
|
`metrics.maintenance_control.primary_control` summarizes the highest-priority
|
||||||
|
source state:
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `blocked_source` | At least one maintenance source found work that could not be admitted or is blocked by a downstream queue. |
|
||||||
|
| `deferred_source` | At least one source was deferred by a partial scanner cycle or budget-limited pass. |
|
||||||
|
| `active_source` | At least one source has current-cycle work or queued downstream work. |
|
||||||
|
| `pacing_pressure` | No source-specific state dominated, but scanner pacing pressure is still visible. |
|
||||||
|
| `none` | No source-level maintenance control pressure was observed. |
|
||||||
|
|
||||||
|
Each `metrics.maintenance_control.sources[]` entry has:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `source` | Scanner source such as `usage`, `lifecycle`, `bucket_replication`, `site_replication`, `heal`, `bitrot`, or `alerts`. |
|
||||||
|
| `state` | `idle`, `active`, `deferred`, or `blocked`. |
|
||||||
|
| `reason` | Derived reason such as `active_work`, `queued_work`, `partial_cycle`, `missed_work`, `transition_queue_backlog`, or `transition_queue_full`. |
|
||||||
|
| `backlog` | Current source-level backlog estimate from queued or missed work. |
|
||||||
|
| `current_checked` | Current-cycle checked work for this source, or the last completed cycle when no scan cycle is active. |
|
||||||
|
| `current_queued` | Current-cycle queued work for this source, or the last completed cycle when no scan cycle is active. |
|
||||||
|
| `current_missed` | Current-cycle work that could not be admitted, or the last completed cycle when no scan cycle is active. |
|
||||||
|
| `lifetime_missed` | Lifetime missed work counter for context. |
|
||||||
|
| `partial_cycles` | Partial cycles attributed to this source. |
|
||||||
|
|
||||||
|
Use this snapshot before changing scanner controls. For example,
|
||||||
|
`blocked_source` with `lifecycle/missed_work` points at downstream lifecycle
|
||||||
|
admission, while `deferred_source` with `usage/partial_cycle` points at scanner
|
||||||
|
cycle budgets.
|
||||||
|
|
||||||
## Reading Distributed Metrics
|
## Reading Distributed Metrics
|
||||||
|
|
||||||
`/rustfs/admin/v3/scanner/status` and `/rustfs/admin/v3/metrics` report the
|
`/rustfs/admin/v3/scanner/status` and `/rustfs/admin/v3/metrics` report the
|
||||||
@@ -169,11 +209,12 @@ done
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `aggregated.scanner` payload preserves the same scanner progress,
|
The `aggregated.scanner` payload preserves the same scanner progress,
|
||||||
checkpoint, pacing, source work, and lifecycle transition fields used by the
|
checkpoint, pacing, source work, maintenance control, and lifecycle transition
|
||||||
local scanner status, but only for the node that returned the response. The
|
fields used by the local scanner status, but only for the node that returned
|
||||||
`by_host.*.scanner` payload keeps that node's host view. Compare the per-node
|
the response. The `by_host.*.scanner` payload keeps that node's host view.
|
||||||
artifacts externally to find old active paths, partial checkpoints, pacing
|
Compare the per-node artifacts externally to find old active paths, partial
|
||||||
pressure, or downstream queue admission problems across the deployment.
|
checkpoints, pacing pressure, source-level control pressure, or downstream
|
||||||
|
queue admission problems across the deployment.
|
||||||
|
|
||||||
## Reading Lifecycle Transition Status
|
## Reading Lifecycle Transition Status
|
||||||
|
|
||||||
@@ -206,21 +247,23 @@ sustained CPU usage while the scanner is enabled:
|
|||||||
|
|
||||||
1. Read `/v3/scanner/status`.
|
1. Read `/v3/scanner/status`.
|
||||||
2. Check `metrics.pacing_pressure.primary_pressure`.
|
2. Check `metrics.pacing_pressure.primary_pressure`.
|
||||||
3. Check `runtime_config.delay`, `runtime_config.max_wait_seconds`, and
|
3. Check `metrics.maintenance_control.primary_control` and source entries
|
||||||
|
before changing runtime controls.
|
||||||
|
4. Check `runtime_config.delay`, `runtime_config.max_wait_seconds`, and
|
||||||
`runtime_config.cycle_interval_seconds` to confirm the active values and
|
`runtime_config.cycle_interval_seconds` to confirm the active values and
|
||||||
their sources.
|
their sources.
|
||||||
4. Check `metrics.current_cycle_objects_scanned`,
|
5. Check `metrics.current_cycle_objects_scanned`,
|
||||||
`metrics.current_cycle_directories_scanned`, and active paths to confirm the
|
`metrics.current_cycle_directories_scanned`, and active paths to confirm the
|
||||||
scanner is the active work.
|
scanner is the active work.
|
||||||
5. If `primary_pressure` is `throttle_pause` and pause ratios are low, raise
|
6. If `primary_pressure` is `throttle_pause` and pause ratios are low, raise
|
||||||
`scanner.delay` first.
|
`scanner.delay` first.
|
||||||
6. If individual sleeps are too short, raise `scanner.max_wait`.
|
7. If individual sleeps are too short, raise `scanner.max_wait`.
|
||||||
7. If each scan cycle finishes but starts too often, raise `scanner.cycle`.
|
8. If each scan cycle finishes but starts too often, raise `scanner.cycle`.
|
||||||
8. If scans must be broken into bounded chunks, set one of the cycle budgets:
|
9. If scans must be broken into bounded chunks, set one of the cycle budgets:
|
||||||
`scanner.cycle_max_duration`, `scanner.cycle_max_objects`, or
|
`scanner.cycle_max_duration`, `scanner.cycle_max_objects`, or
|
||||||
`scanner.cycle_max_directories`.
|
`scanner.cycle_max_directories`.
|
||||||
9. Recheck `pacing_pressure`, source work, and lifecycle transition status after
|
10. Recheck `pacing_pressure`, `maintenance_control`, source work, and
|
||||||
one or more scanner cycles.
|
lifecycle transition status after one or more scanner cycles.
|
||||||
|
|
||||||
Do not rely only on a longer cycle interval if lifecycle, replication, heal, or
|
Do not rely only on a longer cycle interval if lifecycle, replication, heal, or
|
||||||
bitrot work must keep moving. Use source work and transition status to confirm
|
bitrot work must keep moving. Use source work and transition status to confirm
|
||||||
|
|||||||
Reference in New Issue
Block a user