mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-23 04:39:04 +00:00
fix(heal): preserve progress status across nodes
This commit is contained in:
@@ -1089,7 +1089,9 @@ impl PeerRestClient {
|
||||
.await?
|
||||
.max_decoding_message_size(BACKGROUND_HEAL_STATUS_MAX_MESSAGE_SIZE);
|
||||
let response = match client
|
||||
.background_heal_status(Request::new(BackgroundHealStatusRequest::default()))
|
||||
.background_heal_status(Request::new(BackgroundHealStatusRequest {
|
||||
protocol_version: rustfs_protos::BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_inner(),
|
||||
|
||||
@@ -892,7 +892,7 @@ impl ErasureSetHealer {
|
||||
let counter_ok = increment_counter(processed_objects);
|
||||
completed_in_page = completed_in_page.saturating_add(1);
|
||||
counter!("rustfs_heal_skipped_new_versions_total").increment(1);
|
||||
let (skipped_new, skipped_ilm, counter_unknown) = {
|
||||
let progress = {
|
||||
let mut progress = self.progress.write().await;
|
||||
progress.record_skipped_new_version();
|
||||
progress.set_current_object(Some(format!("skipped_new: {bucket}/{}", item.name)));
|
||||
@@ -906,22 +906,22 @@ impl ErasureSetHealer {
|
||||
if !counter_ok {
|
||||
progress.mark_unknown();
|
||||
}
|
||||
(progress.skipped_new_versions, progress.skipped_ilm_expired, progress.counter_unknown)
|
||||
progress.clone()
|
||||
};
|
||||
checkpoint_manager
|
||||
.record_object_outcome(CheckpointObjectOutcomeRecord {
|
||||
object: key,
|
||||
outcome: CheckpointObjectOutcome::Processed,
|
||||
successful: *successful_objects,
|
||||
failed: *failed_objects,
|
||||
skipped: *skipped_objects,
|
||||
bytes: bytes_processed,
|
||||
skipped_new_versions: skipped_new,
|
||||
skipped_ilm_expired: skipped_ilm,
|
||||
counter_unknown: !counter_ok || counter_unknown,
|
||||
successful: progress.objects_healed,
|
||||
failed: progress.objects_failed,
|
||||
skipped: progress.skipped_objects,
|
||||
bytes: progress.bytes_processed,
|
||||
skipped_new_versions: progress.skipped_new_versions,
|
||||
skipped_ilm_expired: progress.skipped_ilm_expired,
|
||||
counter_unknown: progress.counter_unknown,
|
||||
})
|
||||
.await?;
|
||||
if !counter_ok || counter_unknown {
|
||||
if progress.counter_unknown {
|
||||
resume_manager.mark_counter_unknown().await?;
|
||||
}
|
||||
debug!(
|
||||
@@ -957,7 +957,7 @@ impl ErasureSetHealer {
|
||||
let counter_ok = increment_counter(processed_objects);
|
||||
completed_in_page = completed_in_page.saturating_add(1);
|
||||
counter!("rustfs_heal_skipped_ilm_expired_total").increment(1);
|
||||
let (skipped_new, skipped_ilm, counter_unknown) = {
|
||||
let progress = {
|
||||
let mut progress = self.progress.write().await;
|
||||
progress.record_skipped_ilm_expired();
|
||||
progress.set_current_object(Some(format!("skipped_ilm: {bucket}/{}", item.name)));
|
||||
@@ -971,22 +971,22 @@ impl ErasureSetHealer {
|
||||
if !counter_ok {
|
||||
progress.mark_unknown();
|
||||
}
|
||||
(progress.skipped_new_versions, progress.skipped_ilm_expired, progress.counter_unknown)
|
||||
progress.clone()
|
||||
};
|
||||
checkpoint_manager
|
||||
.record_object_outcome(CheckpointObjectOutcomeRecord {
|
||||
object: key,
|
||||
outcome: CheckpointObjectOutcome::Processed,
|
||||
successful: *successful_objects,
|
||||
failed: *failed_objects,
|
||||
skipped: *skipped_objects,
|
||||
bytes: bytes_processed,
|
||||
skipped_new_versions: skipped_new,
|
||||
skipped_ilm_expired: skipped_ilm,
|
||||
counter_unknown: !counter_ok || counter_unknown,
|
||||
successful: progress.objects_healed,
|
||||
failed: progress.objects_failed,
|
||||
skipped: progress.skipped_objects,
|
||||
bytes: progress.bytes_processed,
|
||||
skipped_new_versions: progress.skipped_new_versions,
|
||||
skipped_ilm_expired: progress.skipped_ilm_expired,
|
||||
counter_unknown: progress.counter_unknown,
|
||||
})
|
||||
.await?;
|
||||
if !counter_ok || counter_unknown {
|
||||
if progress.counter_unknown {
|
||||
resume_manager.mark_counter_unknown().await?;
|
||||
}
|
||||
debug!(
|
||||
@@ -1188,7 +1188,7 @@ impl ErasureSetHealer {
|
||||
|
||||
telemetry_unknown |= !increment_counter(processed_objects);
|
||||
completed_in_page += 1;
|
||||
let (progress_unknown, skipped_new_versions, skipped_ilm_expired) = {
|
||||
let progress = {
|
||||
let mut progress = self.progress.write().await;
|
||||
progress.set_current_object(Some(format!("{bucket}/{object}")));
|
||||
progress.update_object_progress(
|
||||
@@ -1201,22 +1201,22 @@ impl ErasureSetHealer {
|
||||
if telemetry_unknown {
|
||||
progress.mark_unknown();
|
||||
}
|
||||
(progress.counter_unknown, progress.skipped_new_versions, progress.skipped_ilm_expired)
|
||||
progress.clone()
|
||||
};
|
||||
checkpoint_manager
|
||||
.record_object_outcome(CheckpointObjectOutcomeRecord {
|
||||
object: key,
|
||||
outcome: checkpoint_outcome,
|
||||
successful: *successful_objects,
|
||||
failed: *failed_objects,
|
||||
skipped: *skipped_objects,
|
||||
bytes: bytes_processed,
|
||||
skipped_new_versions,
|
||||
skipped_ilm_expired,
|
||||
counter_unknown: telemetry_unknown || progress_unknown,
|
||||
successful: progress.objects_healed,
|
||||
failed: progress.objects_failed,
|
||||
skipped: progress.skipped_objects,
|
||||
bytes: progress.bytes_processed,
|
||||
skipped_new_versions: progress.skipped_new_versions,
|
||||
skipped_ilm_expired: progress.skipped_ilm_expired,
|
||||
counter_unknown: progress.counter_unknown,
|
||||
})
|
||||
.await?;
|
||||
if telemetry_unknown || progress_unknown {
|
||||
if progress.counter_unknown {
|
||||
resume_manager.mark_counter_unknown().await?;
|
||||
}
|
||||
|
||||
@@ -1506,8 +1506,8 @@ mod resume_loop_tests {
|
||||
};
|
||||
use crate::heal::progress::HealProgress;
|
||||
use crate::heal::resume::{
|
||||
CheckpointManager, CheckpointObjectOutcome, RESUME_CHECKPOINT_FILE, ReplacementTargetIdentity, ResumeDeleteFailure,
|
||||
ResumeManager, ResumeUtils, compose_key,
|
||||
CheckpointManager, CheckpointObjectOutcome, CheckpointObjectOutcomeRecord, RESUME_CHECKPOINT_FILE,
|
||||
ReplacementTargetIdentity, ResumeDeleteFailure, ResumeManager, ResumeUtils, compose_key,
|
||||
};
|
||||
use crate::heal::storage::{HealLifecycleExpiryContext, HealListItem, HealObjectInfo, HealStorageAPI};
|
||||
use crate::heal::storage_api::status::BucketInfo;
|
||||
|
||||
@@ -2011,91 +2011,11 @@ impl HealManager {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut snapshot = HealProgress::default();
|
||||
let mut has_object_sweep = false;
|
||||
let mut all_object_baselines_known = true;
|
||||
let mut counter_overflow = false;
|
||||
let mut stage_current = 0_u64;
|
||||
let mut stage_total = 0_u64;
|
||||
let mut progresses = Vec::with_capacity(active_tasks.len());
|
||||
for task in active_tasks {
|
||||
let progress = task.get_progress().await;
|
||||
let object_sweep = matches!(progress.kind, crate::heal::progress::HealProgressKind::ObjectSweep);
|
||||
has_object_sweep |= object_sweep;
|
||||
if object_sweep {
|
||||
all_object_baselines_known &= progress.baseline_known;
|
||||
}
|
||||
counter_overflow |=
|
||||
progress.counter_unknown || matches!(progress.progress_state, crate::heal::progress::HealProgressState::Unknown);
|
||||
match stage_current.checked_add(progress.stage_current) {
|
||||
Some(sum) => stage_current = sum,
|
||||
None => counter_overflow = true,
|
||||
}
|
||||
match stage_total.checked_add(progress.stage_total) {
|
||||
Some(sum) => stage_total = sum,
|
||||
None => counter_overflow = true,
|
||||
}
|
||||
for (target, value) in [
|
||||
(&mut snapshot.objects_scanned, progress.objects_scanned),
|
||||
(&mut snapshot.objects_healed, progress.objects_healed),
|
||||
(&mut snapshot.objects_failed, progress.objects_failed),
|
||||
(&mut snapshot.skipped_objects, progress.skipped_objects),
|
||||
(&mut snapshot.skipped_new_versions, progress.skipped_new_versions),
|
||||
(&mut snapshot.skipped_ilm_expired, progress.skipped_ilm_expired),
|
||||
(&mut snapshot.objects_total_count, progress.objects_total_count),
|
||||
(&mut snapshot.objects_total_size, progress.objects_total_size),
|
||||
(&mut snapshot.bytes_processed, progress.bytes_processed),
|
||||
] {
|
||||
match target.checked_add(value) {
|
||||
Some(sum) => *target = sum,
|
||||
None => counter_overflow = true,
|
||||
}
|
||||
}
|
||||
snapshot.start_time = match (snapshot.start_time, progress.start_time) {
|
||||
(Some(current), Some(next)) => Some(current.min(next)),
|
||||
(None, next) => next,
|
||||
(current, None) => current,
|
||||
};
|
||||
snapshot.last_update_time = match (snapshot.last_update_time, progress.last_update_time) {
|
||||
(Some(current), Some(next)) => Some(current.max(next)),
|
||||
(None, next) => next,
|
||||
(current, None) => current,
|
||||
};
|
||||
if progress.current_object.is_some() {
|
||||
snapshot.current_object = progress.current_object;
|
||||
}
|
||||
progresses.push(task.get_progress().await);
|
||||
}
|
||||
snapshot.kind = if has_object_sweep {
|
||||
crate::heal::progress::HealProgressKind::ObjectSweep
|
||||
} else {
|
||||
crate::heal::progress::HealProgressKind::Stage
|
||||
};
|
||||
snapshot.stage_current = stage_current;
|
||||
snapshot.stage_total = stage_total;
|
||||
snapshot.baseline_known = has_object_sweep && all_object_baselines_known;
|
||||
snapshot.progress_state = if counter_overflow {
|
||||
crate::heal::progress::HealProgressState::Unknown
|
||||
} else if has_object_sweep && !all_object_baselines_known {
|
||||
crate::heal::progress::HealProgressState::Indeterminate
|
||||
} else if has_object_sweep {
|
||||
crate::heal::progress::HealProgressState::Running
|
||||
} else if stage_total == 0 {
|
||||
crate::heal::progress::HealProgressState::Indeterminate
|
||||
} else {
|
||||
crate::heal::progress::HealProgressState::Running
|
||||
};
|
||||
if counter_overflow {
|
||||
snapshot.progress_percentage = 0.0;
|
||||
} else if !has_object_sweep {
|
||||
snapshot.progress_percentage = if stage_total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((stage_current as f64 / stage_total as f64) * 100.0).min(99.999)
|
||||
};
|
||||
} else {
|
||||
snapshot.refresh_progress_percentage();
|
||||
}
|
||||
snapshot.refresh_estimated_completion_time();
|
||||
Some(snapshot)
|
||||
crate::heal::progress::aggregate_heal_progress(progresses)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ pub enum HealProgressState {
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HealProgress {
|
||||
#[serde(default)]
|
||||
pub kind: HealProgressKind,
|
||||
@@ -382,6 +382,101 @@ impl HealProgress {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggregate_heal_progress(progresses: impl IntoIterator<Item = HealProgress>) -> Option<HealProgress> {
|
||||
let mut snapshot = HealProgress::default();
|
||||
let mut found = false;
|
||||
let mut has_object_sweep = false;
|
||||
let mut all_object_baselines_known = true;
|
||||
let mut baseline_generation = None;
|
||||
let mut baseline_generation_consistent = true;
|
||||
let mut all_ledgers_complete = true;
|
||||
let mut counter_overflow = false;
|
||||
|
||||
for progress in progresses {
|
||||
found = true;
|
||||
let object_sweep = matches!(progress.kind, HealProgressKind::ObjectSweep);
|
||||
has_object_sweep |= object_sweep;
|
||||
all_ledgers_complete &= progress.ledger_complete;
|
||||
if object_sweep {
|
||||
all_object_baselines_known &= progress.baseline_known;
|
||||
match baseline_generation {
|
||||
None => baseline_generation = Some(progress.baseline_generation),
|
||||
Some(generation) => baseline_generation_consistent &= generation == progress.baseline_generation,
|
||||
}
|
||||
}
|
||||
counter_overflow |= progress.counter_unknown || matches!(progress.progress_state, HealProgressState::Unknown);
|
||||
for (target, value) in [
|
||||
(&mut snapshot.objects_scanned, progress.objects_scanned),
|
||||
(&mut snapshot.objects_healed, progress.objects_healed),
|
||||
(&mut snapshot.objects_failed, progress.objects_failed),
|
||||
(&mut snapshot.skipped_objects, progress.skipped_objects),
|
||||
(&mut snapshot.skipped_new_versions, progress.skipped_new_versions),
|
||||
(&mut snapshot.skipped_ilm_expired, progress.skipped_ilm_expired),
|
||||
(&mut snapshot.objects_total_count, progress.objects_total_count),
|
||||
(&mut snapshot.objects_total_size, progress.objects_total_size),
|
||||
(&mut snapshot.bytes_processed, progress.bytes_processed),
|
||||
(&mut snapshot.stage_current, progress.stage_current),
|
||||
(&mut snapshot.stage_total, progress.stage_total),
|
||||
] {
|
||||
match target.checked_add(value) {
|
||||
Some(sum) => *target = sum,
|
||||
None => {
|
||||
*target = u64::MAX;
|
||||
counter_overflow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.start_time = match (snapshot.start_time, progress.start_time) {
|
||||
(Some(current), Some(next)) => Some(current.min(next)),
|
||||
(None, next) => next,
|
||||
(current, None) => current,
|
||||
};
|
||||
snapshot.last_update_time = match (snapshot.last_update_time, progress.last_update_time) {
|
||||
(Some(current), Some(next)) => Some(current.max(next)),
|
||||
(None, next) => next,
|
||||
(current, None) => current,
|
||||
};
|
||||
if progress.current_object.is_some() {
|
||||
snapshot.current_object = progress.current_object;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return None;
|
||||
}
|
||||
|
||||
snapshot.kind = if has_object_sweep {
|
||||
HealProgressKind::ObjectSweep
|
||||
} else {
|
||||
HealProgressKind::Stage
|
||||
};
|
||||
snapshot.baseline_known = has_object_sweep && all_object_baselines_known && baseline_generation_consistent;
|
||||
snapshot.baseline_generation = if snapshot.baseline_known && baseline_generation_consistent {
|
||||
baseline_generation.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
snapshot.ledger_complete = all_ledgers_complete;
|
||||
snapshot.counter_unknown = counter_overflow;
|
||||
if counter_overflow {
|
||||
snapshot.progress_state = HealProgressState::Unknown;
|
||||
snapshot.progress_percentage = if snapshot.ledger_complete { 100.0 } else { 0.0 };
|
||||
} else if snapshot.ledger_complete {
|
||||
snapshot.progress_state = HealProgressState::Completed;
|
||||
snapshot.progress_percentage = 100.0;
|
||||
} else if has_object_sweep {
|
||||
snapshot.refresh_progress_percentage();
|
||||
} else if snapshot.stage_total == 0 {
|
||||
snapshot.progress_state = HealProgressState::Indeterminate;
|
||||
snapshot.progress_percentage = 0.0;
|
||||
} else {
|
||||
snapshot.progress_state = HealProgressState::Running;
|
||||
snapshot.progress_percentage = ((snapshot.stage_current as f64 / snapshot.stage_total as f64) * 100.0).min(99.999);
|
||||
}
|
||||
snapshot.refresh_estimated_completion_time();
|
||||
Some(snapshot)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealStatistics {
|
||||
/// Total heal tasks
|
||||
@@ -725,6 +820,31 @@ mod tests {
|
||||
progress.mark_completed();
|
||||
assert!(progress.is_completed());
|
||||
assert_eq!(progress.progress_state, HealProgressState::Unknown);
|
||||
|
||||
let aggregate = aggregate_heal_progress([progress]).expect("progress should aggregate");
|
||||
assert!(aggregate.ledger_complete);
|
||||
assert!(aggregate.counter_unknown);
|
||||
assert_eq!(aggregate.progress_state, HealProgressState::Unknown);
|
||||
assert_eq!(aggregate.progress_percentage, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_rejects_mixed_baseline_generations() {
|
||||
let progress = |generation| HealProgress {
|
||||
kind: HealProgressKind::ObjectSweep,
|
||||
objects_scanned: 5,
|
||||
objects_total_count: 10,
|
||||
progress_state: HealProgressState::Running,
|
||||
baseline_generation: Some(generation),
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let aggregate = aggregate_heal_progress([progress(1), progress(2)]).expect("progress should aggregate");
|
||||
assert!(!aggregate.baseline_known);
|
||||
assert_eq!(aggregate.baseline_generation, None);
|
||||
assert_eq!(aggregate.progress_state, HealProgressState::Indeterminate);
|
||||
assert_eq!(aggregate.progress_percentage, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -29,10 +29,10 @@ use super::{
|
||||
|
||||
const EVENT_HEAL_CHECKPOINT_STATE: &str = "heal_checkpoint_state";
|
||||
|
||||
/// Current on-disk schema version for `ResumeCheckpoint`. Same rationale as
|
||||
/// `CURRENT_RESUME_SCHEMA`: pre-per-version dedup identities are not comparable
|
||||
/// to the new `compose_key` identities, so a stale checkpoint is discarded.
|
||||
pub(super) const CURRENT_CHECKPOINT_SCHEMA: u32 = 5;
|
||||
/// Current on-disk schema version for `ResumeCheckpoint`. Schema 5 could
|
||||
/// persist dedup identities without the aggregate counters needed to restore
|
||||
/// them safely, so stale checkpoints are discarded and replayed.
|
||||
pub(super) const CURRENT_CHECKPOINT_SCHEMA: u32 = 6;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CheckpointObjectOutcome {
|
||||
@@ -252,10 +252,9 @@ impl CheckpointManager {
|
||||
});
|
||||
}
|
||||
|
||||
// A checkpoint from an older schema stored latest-only dedup identities
|
||||
// that are not comparable to the new per-version `compose_key`
|
||||
// identities. Discard the stale sets and position, then stamp the
|
||||
// current schema so the scan restarts cleanly.
|
||||
// Older checkpoints can contain identities that are not comparable to
|
||||
// the current keys or lack their corresponding aggregate counters.
|
||||
// Discard the stale sets and position so the scan restarts cleanly.
|
||||
if checkpoint.schema_version > CURRENT_CHECKPOINT_SCHEMA {
|
||||
return Err(Error::TaskExecutionFailed {
|
||||
message: format!(
|
||||
|
||||
@@ -1603,14 +1603,14 @@ async fn test_resumestate_schema_v0_discarded_on_load() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_checkpoint_schema_v4_discarded_on_load() {
|
||||
async fn test_checkpoint_schema_v5_discarded_on_load() {
|
||||
let (temp_dir, disk) = schema_test_disk().await;
|
||||
|
||||
// The previous checkpoint schema is unsafe once its paired resume
|
||||
// state is discarded: retaining either position would skip work.
|
||||
// Schema v5 can persist failed identities without the aggregate counters
|
||||
// that make those identities safe to deduplicate after an upgrade.
|
||||
let task_id = "00000000-0000-4000-8000-000000000002";
|
||||
let legacy = r#"{
|
||||
"schema_version": 4,
|
||||
"schema_version": 5,
|
||||
"task_id": "00000000-0000-4000-8000-000000000002",
|
||||
"checkpoint_time": 1700000000,
|
||||
"current_bucket_index": 2,
|
||||
|
||||
@@ -445,6 +445,9 @@ impl HealTask {
|
||||
}
|
||||
|
||||
pub(super) async fn apply_erasure_set_usage_baseline(&self, buckets: &[String], set_disk_id: &str) -> Result<()> {
|
||||
if matches!(self.options.scan_mode, HealScanMode::Deep) || matches!(self.source, HealRequestSource::AutoHeal) {
|
||||
return Ok(());
|
||||
}
|
||||
let baseline = match self
|
||||
.await_with_control(self.storage.erasure_set_usage_baseline(buckets))
|
||||
.await
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::super::storage_api::status::BucketInfo;
|
||||
use crate::heal::progress::HealProgressState;
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_request_carries_remaining_timeout_budget() {
|
||||
@@ -2154,6 +2155,49 @@ async fn erasure_set_heal_applies_usage_baseline_to_progress() {
|
||||
assert!((progress.progress_percentage - 25.0).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn erasure_set_disk_walk_keeps_cluster_usage_baseline_indeterminate() {
|
||||
for (scan_mode, source) in [
|
||||
(HealScanMode::Deep, HealRequestSource::Admin),
|
||||
(HealScanMode::Normal, HealRequestSource::AutoHeal),
|
||||
] {
|
||||
let temp = TempDir::new().expect("temporary directory should be created");
|
||||
let disk = make_resume_disk(&temp).await;
|
||||
let storage = Arc::new(MockStorage {
|
||||
resume_disk: Mutex::new(Some(disk)),
|
||||
usage_baseline: Mutex::new(Some(HealBucketUsageBaseline {
|
||||
objects_count: 10,
|
||||
bytes: 8,
|
||||
generation: Some(1),
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
let mut request = HealRequest::new(
|
||||
HealType::ErasureSet {
|
||||
buckets: vec!["bucket-a".to_string()],
|
||||
set_disk_id: "pool_0_set_0".to_string(),
|
||||
},
|
||||
HealOptions {
|
||||
scan_mode,
|
||||
timeout: None,
|
||||
..Default::default()
|
||||
},
|
||||
HealPriority::Normal,
|
||||
);
|
||||
request.source = source;
|
||||
let task = HealTask::from_request(request, storage);
|
||||
|
||||
task.heal_erasure_set(vec!["bucket-a".to_string()], "pool_0_set_0".to_string())
|
||||
.await
|
||||
.expect("erasure set heal should complete");
|
||||
|
||||
let progress = task.get_progress().await;
|
||||
assert!(!progress.baseline_known);
|
||||
assert_eq!(progress.baseline_generation, None);
|
||||
assert_eq!(progress.progress_state, HealProgressState::Indeterminate);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn erasure_set_heal_ignores_usage_baseline_errors() {
|
||||
let temp = TempDir::new().expect("temporary directory should be created");
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use error::{Error, Result};
|
||||
pub use heal::{
|
||||
HealManager, HealOperationsSnapshot, HealOptions, HealPriority, HealPriorityCounts, HealRequest, HealSourceCounts, HealType,
|
||||
channel::HealChannelProcessor,
|
||||
progress::HealProgress,
|
||||
progress::{HealProgress, aggregate_heal_progress},
|
||||
resume::{ReplacementRecoveryRecord, ReplacementRecoveryState, ResumeUtils},
|
||||
};
|
||||
use rustfs_concurrency::WorkloadAdmissionSnapshotProvider;
|
||||
|
||||
@@ -1215,7 +1215,10 @@ pub struct ScannerActivityResponse {
|
||||
pub dirty_usage_pending: bool,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct BackgroundHealStatusRequest {}
|
||||
pub struct BackgroundHealStatusRequest {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub protocol_version: u32,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct BackgroundHealStatusResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
|
||||
@@ -170,6 +170,7 @@ pub fn internode_rpc_max_message_size() -> usize {
|
||||
pub const HEAL_CONTROL_RPC_MAX_MESSAGE_SIZE: usize = heal_control::RESULT_MAX_SIZE + 1024;
|
||||
pub const HEAL_CONTROL_PROTOCOL_VERSION: u32 = 3;
|
||||
pub const DYNAMIC_CONFIG_PROTOCOL_VERSION: u32 = 1;
|
||||
pub const BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION: u32 = 2;
|
||||
pub const HEAL_CONTROL_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-heal-control-capability-v3\0";
|
||||
pub const REMOTE_VERSION_STATE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-tier-remote-version-state-capability-v1\0";
|
||||
pub const CROSS_POOL_FENCE_CAPABILITY_PROBE_PREFIX: &[u8] = b"rustfs-cross-pool-fence-capability-v1\0";
|
||||
@@ -2346,10 +2347,28 @@ pub async fn evict_failed_connection_with_log_level(addr: &str, log_level: Conne
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use prost::Message as _;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static INTERNODE_RPC_MSGPACK_ONLY_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[derive(Clone, PartialEq, prost::Message)]
|
||||
struct BackgroundHealStatusRequestV1 {}
|
||||
|
||||
#[test]
|
||||
fn background_heal_status_request_remains_rolling_upgrade_compatible() {
|
||||
let current = proto_gen::node_service::BackgroundHealStatusRequest {
|
||||
protocol_version: BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION,
|
||||
};
|
||||
let encoded = current.encode_to_vec();
|
||||
BackgroundHealStatusRequestV1::decode(encoded.as_slice()).expect("v1 server should ignore the version field");
|
||||
|
||||
let encoded = BackgroundHealStatusRequestV1 {}.encode_to_vec();
|
||||
let decoded = proto_gen::node_service::BackgroundHealStatusRequest::decode(encoded.as_slice())
|
||||
.expect("v2 server should accept a v1 request");
|
||||
assert_eq!(decoded.protocol_version, 0);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
struct CompatPayloadField {
|
||||
message: &'static str,
|
||||
|
||||
@@ -846,7 +846,9 @@ message ScannerActivityResponse {
|
||||
bool dirty_usage_pending = 9;
|
||||
}
|
||||
|
||||
message BackgroundHealStatusRequest {}
|
||||
message BackgroundHealStatusRequest {
|
||||
uint32 protocol_version = 1;
|
||||
}
|
||||
|
||||
message BackgroundHealStatusResponse {
|
||||
bool success = 1;
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::admin::storage_api::bucket::utils::is_valid_object_prefix;
|
||||
use crate::server::ADMIN_PREFIX;
|
||||
use crate::server::RemoteAddr;
|
||||
use crate::storage::rpc::node_service::heal::{
|
||||
HealControlCoordinator, NodeHealProgress, NodeHealStatusSnapshot, capture_node_heal_status, decode_node_heal_status,
|
||||
HealControlCoordinator, NodeHealStatusSnapshot, capture_node_heal_status, decode_node_heal_status,
|
||||
decode_node_replacement_recovery_status, heal_control_coordinator, heal_topology_fingerprint,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
@@ -298,14 +298,7 @@ fn background_heal_runtime_state(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BackgroundHealProgress {
|
||||
objects_scanned: u64,
|
||||
objects_healed: u64,
|
||||
objects_failed: u64,
|
||||
bytes_processed: u64,
|
||||
}
|
||||
type BackgroundHealProgress = rustfs_heal::HealProgress;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClusterHealStatusSnapshot {
|
||||
@@ -344,17 +337,10 @@ fn add_operations(total: &mut rustfs_heal::HealOperationsSnapshot, next: rustfs_
|
||||
add_source_counts(&mut total.retrying_by_source, next.retrying_by_source);
|
||||
}
|
||||
|
||||
fn add_progress(total: &mut BackgroundHealProgress, next: NodeHealProgress) {
|
||||
total.objects_scanned = total.objects_scanned.saturating_add(next.objects_scanned);
|
||||
total.objects_healed = total.objects_healed.saturating_add(next.objects_healed);
|
||||
total.objects_failed = total.objects_failed.saturating_add(next.objects_failed);
|
||||
total.bytes_processed = total.bytes_processed.saturating_add(next.bytes_processed);
|
||||
}
|
||||
|
||||
fn aggregate_cluster_heal_status(snapshots: Vec<NodeHealStatusSnapshot>) -> ClusterHealStatusSnapshot {
|
||||
let mut info = BackgroundHealInfo::default();
|
||||
let mut operations = rustfs_heal::HealOperationsSnapshot::default();
|
||||
let mut progress = None;
|
||||
let mut progress = Vec::new();
|
||||
let mut any_services_enabled = false;
|
||||
let mut any_initialized = false;
|
||||
|
||||
@@ -371,18 +357,12 @@ fn aggregate_cluster_heal_status(snapshots: Vec<NodeHealStatusSnapshot>) -> Clus
|
||||
}
|
||||
add_operations(&mut operations, snapshot.operations);
|
||||
if let Some(next) = snapshot.progress {
|
||||
add_progress(
|
||||
progress.get_or_insert(BackgroundHealProgress {
|
||||
objects_scanned: 0,
|
||||
objects_healed: 0,
|
||||
objects_failed: 0,
|
||||
bytes_processed: 0,
|
||||
}),
|
||||
next,
|
||||
);
|
||||
progress.push(next);
|
||||
}
|
||||
}
|
||||
|
||||
let progress = rustfs_heal::aggregate_heal_progress(progress);
|
||||
|
||||
let state = if operations.queue_length > 0 || operations.active_tasks > 0 || operations.retrying_tasks > 0 {
|
||||
HealRuntimeState::Active
|
||||
} else if any_initialized {
|
||||
@@ -2201,10 +2181,19 @@ mod tests {
|
||||
};
|
||||
|
||||
let progress = BackgroundHealProgress {
|
||||
kind: rustfs_heal::heal::progress::HealProgressKind::ObjectSweep,
|
||||
objects_scanned: 7,
|
||||
objects_healed: 3,
|
||||
objects_failed: 1,
|
||||
skipped_objects: 3,
|
||||
objects_total_count: 10,
|
||||
objects_total_size: 8192,
|
||||
bytes_processed: 4096,
|
||||
progress_percentage: 50.0,
|
||||
progress_state: rustfs_heal::heal::progress::HealProgressState::Running,
|
||||
baseline_generation: Some(42),
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let encoded = encode_background_heal_status(
|
||||
@@ -2220,7 +2209,14 @@ mod tests {
|
||||
assert_eq!(json["progress"]["objectsScanned"], 7);
|
||||
assert_eq!(json["progress"]["objectsHealed"], 3);
|
||||
assert_eq!(json["progress"]["objectsFailed"], 1);
|
||||
assert_eq!(json["progress"]["skippedObjects"], 3);
|
||||
assert_eq!(json["progress"]["objectsTotalCount"], 10);
|
||||
assert_eq!(json["progress"]["objectsTotalSize"], 8192);
|
||||
assert_eq!(json["progress"]["bytesProcessed"], 4096);
|
||||
assert_eq!(json["progress"]["progressState"], "running");
|
||||
assert_eq!(json["progress"]["baselineGeneration"], 42);
|
||||
assert_eq!(json["progress"]["baselineKnown"], true);
|
||||
assert_eq!(json["progress"]["counterUnknown"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2242,10 +2238,18 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
Some(NodeHealProgress {
|
||||
kind: rustfs_heal::heal::progress::HealProgressKind::ObjectSweep,
|
||||
objects_scanned: 3,
|
||||
objects_healed: 1,
|
||||
objects_failed: 0,
|
||||
skipped_objects: 2,
|
||||
objects_total_count: 6,
|
||||
objects_total_size: 400,
|
||||
bytes_processed: 100,
|
||||
progress_state: rustfs_heal::heal::progress::HealProgressState::Running,
|
||||
baseline_generation: Some(9),
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
let peer = NodeHealStatusSnapshot::for_test(
|
||||
@@ -2265,10 +2269,17 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
Some(NodeHealProgress {
|
||||
kind: rustfs_heal::heal::progress::HealProgressKind::ObjectSweep,
|
||||
objects_scanned: 5,
|
||||
objects_healed: 4,
|
||||
objects_failed: 1,
|
||||
objects_total_count: 4,
|
||||
objects_total_size: 1600,
|
||||
bytes_processed: 900,
|
||||
progress_state: rustfs_heal::heal::progress::HealProgressState::Running,
|
||||
baseline_generation: Some(9),
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2287,7 +2298,15 @@ mod tests {
|
||||
assert_eq!(progress.objects_scanned, 8);
|
||||
assert_eq!(progress.objects_healed, 5);
|
||||
assert_eq!(progress.objects_failed, 1);
|
||||
assert_eq!(progress.skipped_objects, 2);
|
||||
assert_eq!(progress.objects_total_count, 10);
|
||||
assert_eq!(progress.objects_total_size, 2000);
|
||||
assert_eq!(progress.bytes_processed, 1000);
|
||||
assert_eq!(progress.progress_percentage, 50.0);
|
||||
assert_eq!(progress.progress_state, rustfs_heal::heal::progress::HealProgressState::Running);
|
||||
assert_eq!(progress.baseline_generation, Some(9));
|
||||
assert!(progress.baseline_known);
|
||||
assert!(!progress.counter_unknown);
|
||||
|
||||
assert_eq!(peer_first.state, HealRuntimeState::Active);
|
||||
assert_eq!(peer_first.operations, local_first.operations);
|
||||
@@ -2327,6 +2346,7 @@ mod tests {
|
||||
objects_healed: value,
|
||||
objects_failed: value,
|
||||
bytes_processed: value,
|
||||
..Default::default()
|
||||
};
|
||||
let saturated = NodeHealStatusSnapshot::for_test(
|
||||
true,
|
||||
|
||||
@@ -1912,7 +1912,7 @@ impl Node for NodeService {
|
||||
|
||||
async fn background_heal_status(
|
||||
&self,
|
||||
_request: Request<BackgroundHealStatusRequest>,
|
||||
request: Request<BackgroundHealStatusRequest>,
|
||||
) -> Result<Response<BackgroundHealStatusResponse>, Status> {
|
||||
if self.resolve_object_store().is_none() {
|
||||
return Ok(Response::new(BackgroundHealStatusResponse {
|
||||
@@ -1922,7 +1922,7 @@ impl Node for NodeService {
|
||||
}));
|
||||
}
|
||||
let snapshot = heal::capture_node_heal_status(rustfs_scanner::scanner::BackgroundHealInfo::default()).await;
|
||||
match heal::encode_node_heal_status(&snapshot) {
|
||||
match heal::encode_node_heal_status(&snapshot, request.into_inner().protocol_version) {
|
||||
Ok(bg_heal_state) => Ok(Response::new(BackgroundHealStatusResponse {
|
||||
success: true,
|
||||
bg_heal_state: bg_heal_state.into(),
|
||||
|
||||
@@ -25,7 +25,8 @@ use std::io::Cursor;
|
||||
|
||||
use super::super::encode_msgpack_map;
|
||||
|
||||
const NODE_HEAL_STATUS_VERSION: u8 = 1;
|
||||
const NODE_HEAL_STATUS_PREVIOUS_VERSION: u8 = 1;
|
||||
const NODE_HEAL_STATUS_VERSION: u8 = 2;
|
||||
const NODE_HEAL_STATUS_MAX_SIZE: usize = 64 * 1024;
|
||||
const NODE_REPLACEMENT_RECOVERY_STATUS_VERSION: u8 = 1;
|
||||
const NODE_REPLACEMENT_RECOVERY_STATUS_MAX_SIZE: usize = 64 * 1024;
|
||||
@@ -172,13 +173,38 @@ pub(crate) fn heal_topology_fingerprint(endpoint_pools: &EndpointServerPools) ->
|
||||
Ok(hex_simd::encode_to_string(hasher.finalize(), hex_simd::AsciiCase::Lower))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) type NodeHealProgress = rustfs_heal::HealProgress;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct NodeHealProgress {
|
||||
pub objects_scanned: u64,
|
||||
pub objects_healed: u64,
|
||||
pub objects_failed: u64,
|
||||
pub bytes_processed: u64,
|
||||
struct NodeHealProgressV1 {
|
||||
objects_scanned: u64,
|
||||
objects_healed: u64,
|
||||
objects_failed: u64,
|
||||
bytes_processed: u64,
|
||||
}
|
||||
|
||||
impl From<&NodeHealProgress> for NodeHealProgressV1 {
|
||||
fn from(progress: &NodeHealProgress) -> Self {
|
||||
Self {
|
||||
objects_scanned: progress.objects_scanned,
|
||||
objects_healed: progress.objects_healed,
|
||||
objects_failed: progress.objects_failed,
|
||||
bytes_processed: progress.bytes_processed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeHealProgressV1> for NodeHealProgress {
|
||||
fn from(progress: NodeHealProgressV1) -> Self {
|
||||
Self {
|
||||
objects_scanned: progress.objects_scanned,
|
||||
objects_healed: progress.objects_healed,
|
||||
objects_failed: progress.objects_failed,
|
||||
bytes_processed: progress.bytes_processed,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -220,6 +246,48 @@ pub(crate) struct NodeHealStatusSnapshot {
|
||||
pub progress: Option<NodeHealProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct NodeHealStatusSnapshotV1 {
|
||||
version: u8,
|
||||
services_enabled: bool,
|
||||
initialized: bool,
|
||||
info: NodeHealInfo,
|
||||
operations: HealOperationsSnapshot,
|
||||
progress: Option<NodeHealProgressV1>,
|
||||
}
|
||||
|
||||
impl From<&NodeHealStatusSnapshot> for NodeHealStatusSnapshotV1 {
|
||||
fn from(snapshot: &NodeHealStatusSnapshot) -> Self {
|
||||
Self {
|
||||
version: NODE_HEAL_STATUS_PREVIOUS_VERSION,
|
||||
services_enabled: snapshot.services_enabled,
|
||||
initialized: snapshot.initialized,
|
||||
info: snapshot.info.clone(),
|
||||
operations: snapshot.operations,
|
||||
progress: snapshot.progress.as_ref().map(NodeHealProgressV1::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeHealStatusSnapshotV1> for NodeHealStatusSnapshot {
|
||||
fn from(snapshot: NodeHealStatusSnapshotV1) -> Self {
|
||||
Self {
|
||||
version: snapshot.version,
|
||||
services_enabled: snapshot.services_enabled,
|
||||
initialized: snapshot.initialized,
|
||||
info: snapshot.info,
|
||||
operations: snapshot.operations,
|
||||
progress: snapshot.progress.map(NodeHealProgress::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NodeHealStatusVersion {
|
||||
version: u8,
|
||||
}
|
||||
|
||||
impl NodeHealStatusSnapshot {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn for_test(
|
||||
@@ -249,14 +317,7 @@ impl NodeHealStatusSnapshot {
|
||||
}
|
||||
|
||||
pub(crate) async fn capture_node_heal_status(info: BackgroundHealInfo) -> NodeHealStatusSnapshot {
|
||||
let progress = rustfs_heal::current_heal_progress_snapshot()
|
||||
.await
|
||||
.map(|progress| NodeHealProgress {
|
||||
objects_scanned: progress.objects_scanned,
|
||||
objects_healed: progress.objects_healed,
|
||||
objects_failed: progress.objects_failed,
|
||||
bytes_processed: progress.bytes_processed,
|
||||
});
|
||||
let progress = rustfs_heal::current_heal_progress_snapshot().await;
|
||||
|
||||
NodeHealStatusSnapshot {
|
||||
version: NODE_HEAL_STATUS_VERSION,
|
||||
@@ -268,22 +329,43 @@ pub(crate) async fn capture_node_heal_status(info: BackgroundHealInfo) -> NodeHe
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode_node_heal_status(snapshot: &NodeHealStatusSnapshot) -> Result<Vec<u8>, String> {
|
||||
encode_msgpack_map(snapshot).map_err(|err| format!("failed to encode node heal status: {err}"))
|
||||
pub(crate) fn encode_node_heal_status(snapshot: &NodeHealStatusSnapshot, protocol_version: u32) -> Result<Vec<u8>, String> {
|
||||
let encoded = if protocol_version < rustfs_protos::BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION {
|
||||
encode_msgpack_map(&NodeHealStatusSnapshotV1::from(snapshot))
|
||||
} else {
|
||||
let mut snapshot = snapshot.clone();
|
||||
snapshot.version = NODE_HEAL_STATUS_VERSION;
|
||||
encode_msgpack_map(&snapshot)
|
||||
};
|
||||
encoded.map_err(|err| format!("failed to encode node heal status: {err}"))
|
||||
}
|
||||
|
||||
pub(crate) fn decode_node_heal_status(data: &[u8]) -> Result<NodeHealStatusSnapshot, String> {
|
||||
if data.len() > NODE_HEAL_STATUS_MAX_SIZE {
|
||||
return Err("node heal status exceeds size limit".to_string());
|
||||
}
|
||||
let mut deserializer = Deserializer::new(Cursor::new(data));
|
||||
let snapshot = NodeHealStatusSnapshot::deserialize(&mut deserializer)
|
||||
.map_err(|err| format!("failed to decode node heal status: {err}"))?;
|
||||
let decode_version = || {
|
||||
let mut deserializer = Deserializer::new(Cursor::new(data));
|
||||
NodeHealStatusVersion::deserialize(&mut deserializer)
|
||||
.map(|version| (version, deserializer))
|
||||
.map_err(|err| format!("failed to decode node heal status: {err}"))
|
||||
};
|
||||
let (version, deserializer) = decode_version()?;
|
||||
if usize::try_from(deserializer.get_ref().position()).ok() != Some(data.len()) {
|
||||
return Err("node heal status contains trailing data".to_string());
|
||||
}
|
||||
if snapshot.version != NODE_HEAL_STATUS_VERSION {
|
||||
return Err(format!("unsupported node heal status version: {}", snapshot.version));
|
||||
|
||||
let mut deserializer = Deserializer::new(Cursor::new(data));
|
||||
let snapshot = match version.version {
|
||||
NODE_HEAL_STATUS_PREVIOUS_VERSION => {
|
||||
NodeHealStatusSnapshotV1::deserialize(&mut deserializer).map(NodeHealStatusSnapshot::from)
|
||||
}
|
||||
NODE_HEAL_STATUS_VERSION => NodeHealStatusSnapshot::deserialize(&mut deserializer),
|
||||
version => return Err(format!("unsupported node heal status version: {version}")),
|
||||
}
|
||||
.map_err(|err| format!("failed to decode node heal status: {err}"))?;
|
||||
if usize::try_from(deserializer.get_ref().position()).ok() != Some(data.len()) {
|
||||
return Err("node heal status contains trailing data".to_string());
|
||||
}
|
||||
Ok(snapshot)
|
||||
}
|
||||
@@ -387,9 +469,10 @@ pub(crate) fn decode_node_replacement_recovery_status(data: &[u8]) -> Result<Nod
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
NODE_HEAL_STATUS_MAX_SIZE, NODE_HEAL_STATUS_VERSION, NodeHealProgress, NodeHealStatusSnapshot,
|
||||
NodeReplacementRecoveryStatusSnapshot, decode_node_heal_status, decode_node_replacement_recovery_status,
|
||||
encode_node_heal_status, encode_node_replacement_recovery_status, heal_control_coordinator, heal_topology_fingerprint,
|
||||
NODE_HEAL_STATUS_MAX_SIZE, NODE_HEAL_STATUS_PREVIOUS_VERSION, NODE_HEAL_STATUS_VERSION, NodeHealProgress,
|
||||
NodeHealStatusSnapshot, NodeReplacementRecoveryStatusSnapshot, decode_node_heal_status,
|
||||
decode_node_replacement_recovery_status, encode_node_heal_status, encode_node_replacement_recovery_status,
|
||||
heal_control_coordinator, heal_topology_fingerprint,
|
||||
};
|
||||
use crate::storage::storage_api::{
|
||||
Endpoint,
|
||||
@@ -533,20 +616,72 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
Some(NodeHealProgress {
|
||||
kind: rustfs_heal::heal::progress::HealProgressKind::ObjectSweep,
|
||||
objects_scanned: 7,
|
||||
objects_healed: 5,
|
||||
objects_failed: 1,
|
||||
skipped_objects: 1,
|
||||
objects_total_count: 10,
|
||||
objects_total_size: 2048,
|
||||
bytes_processed: 1024,
|
||||
progress_percentage: 50.0,
|
||||
progress_state: rustfs_heal::heal::progress::HealProgressState::Running,
|
||||
baseline_generation: Some(42),
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
let encoded = encode_node_heal_status(&snapshot).expect("snapshot should encode");
|
||||
let encoded = encode_node_heal_status(&snapshot, rustfs_protos::BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION)
|
||||
.expect("snapshot should encode");
|
||||
let decoded = decode_node_heal_status(&encoded).expect("snapshot should decode");
|
||||
assert_eq!(decoded.version, NODE_HEAL_STATUS_VERSION);
|
||||
assert_eq!(decoded.operations.queue_length, 2);
|
||||
assert_eq!(decoded.progress, snapshot.progress);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_heal_status_v1_encoding_preserves_rolling_compatibility() {
|
||||
let snapshot = NodeHealStatusSnapshot::for_test(
|
||||
true,
|
||||
true,
|
||||
BackgroundHealInfo::default(),
|
||||
HealOperationsSnapshot::default(),
|
||||
Some(NodeHealProgress {
|
||||
objects_scanned: 7,
|
||||
objects_healed: 5,
|
||||
objects_failed: 1,
|
||||
skipped_objects: 3,
|
||||
bytes_processed: 1024,
|
||||
progress_state: rustfs_heal::heal::progress::HealProgressState::Running,
|
||||
baseline_known: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
let encoded =
|
||||
encode_node_heal_status(&snapshot, u32::from(NODE_HEAL_STATUS_PREVIOUS_VERSION)).expect("v1 snapshot should encode");
|
||||
let wire: serde_json::Value = rmp_serde::from_slice(&encoded).expect("v1 snapshot should decode as JSON");
|
||||
let progress = wire["progress"].as_object().expect("v1 progress should be a map");
|
||||
assert_eq!(wire["version"], NODE_HEAL_STATUS_PREVIOUS_VERSION);
|
||||
assert_eq!(progress.len(), 4);
|
||||
assert_eq!(progress["objectsScanned"], 7);
|
||||
assert!(!progress.contains_key("skippedObjects"));
|
||||
|
||||
let decoded = decode_node_heal_status(&encoded).expect("v1 snapshot should decode");
|
||||
let progress = decoded.progress.as_ref().expect("v1 progress should be present");
|
||||
assert_eq!(decoded.version, NODE_HEAL_STATUS_PREVIOUS_VERSION);
|
||||
assert_eq!(progress.objects_scanned, 7);
|
||||
assert_eq!(progress.progress_state, rustfs_heal::heal::progress::HealProgressState::Unknown);
|
||||
assert!(!progress.baseline_known);
|
||||
|
||||
let upgraded = encode_node_heal_status(&decoded, rustfs_protos::BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION)
|
||||
.expect("decoded v1 snapshot should upgrade to v2");
|
||||
let upgraded = decode_node_heal_status(&upgraded).expect("upgraded snapshot should decode");
|
||||
assert_eq!(upgraded.version, NODE_HEAL_STATUS_VERSION);
|
||||
assert_eq!(upgraded.progress.as_ref(), Some(progress));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_heal_status_rejects_unknown_version() {
|
||||
let mut snapshot = NodeHealStatusSnapshot::for_test(
|
||||
@@ -558,7 +693,7 @@ mod tests {
|
||||
);
|
||||
snapshot.version += 1;
|
||||
|
||||
let encoded = encode_node_heal_status(&snapshot).expect("snapshot should encode");
|
||||
let encoded = rmp_serde::to_vec_named(&snapshot).expect("snapshot should encode");
|
||||
let err = decode_node_heal_status(&encoded).expect_err("unknown version should fail closed");
|
||||
assert!(err.contains("unsupported node heal status version"));
|
||||
}
|
||||
@@ -579,13 +714,22 @@ mod tests {
|
||||
"activeBySource": {"scanner": 0, "admin": 1, "autoHeal": 0, "internal": 0, "readRepair": 0},
|
||||
"retryingBySource": {"scanner": 0, "admin": 0, "autoHeal": 0, "internal": 0, "readRepair": 0}
|
||||
},
|
||||
"progress": null
|
||||
"progress": {
|
||||
"objectsScanned": 7,
|
||||
"objectsHealed": 5,
|
||||
"objectsFailed": 1,
|
||||
"bytesProcessed": 1024
|
||||
}
|
||||
});
|
||||
let encoded = rmp_serde::to_vec_named(&fixture).expect("fixture should encode");
|
||||
let decoded = decode_node_heal_status(&encoded).expect("fixed v1 fixture should decode");
|
||||
assert_eq!(decoded.info().bitrot_start_cycle, 9);
|
||||
assert_eq!(decoded.operations.queue_length, 2);
|
||||
assert_eq!(decoded.operations.queued_by_source.mrf, 0);
|
||||
let progress = decoded.progress.expect("legacy progress should decode");
|
||||
assert_eq!(progress.objects_scanned, 7);
|
||||
assert!(!progress.baseline_known);
|
||||
assert_eq!(progress.progress_state, rustfs_heal::heal::progress::HealProgressState::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -631,7 +775,8 @@ mod tests {
|
||||
HealOperationsSnapshot::default(),
|
||||
None,
|
||||
);
|
||||
let encoded = encode_node_heal_status(&snapshot).expect("snapshot should encode");
|
||||
let encoded = encode_node_heal_status(&snapshot, rustfs_protos::BACKGROUND_HEAL_STATUS_PROTOCOL_VERSION)
|
||||
.expect("snapshot should encode");
|
||||
let encoded_json: serde_json::Value = rmp_serde::from_slice(&encoded).expect("encoded snapshot should decode as JSON");
|
||||
assert_eq!(encoded_json["info"]["bitrotStartTime"], serde_json::json!("2023-11-14T22:13:20.123456Z"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user