Skip to content

Commit c97a418

Browse files
authored
fix(heal): preserve incomplete and unknown cluster status (#377)
1 parent 1b212cb commit c97a418

6 files changed

Lines changed: 448 additions & 19 deletions

File tree

crates/cli/src/commands/admin/heal.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use super::get_admin_client;
99
use crate::exit_code::ExitCode;
1010
use crate::output::Formatter;
1111
use rc_core::admin::{
12-
AdminApi, HealRuntimeState, HealScanMode, HealStartRequest, HealStatus, HealTaskRequest,
12+
AdminApi, BackgroundHealCoverage, HealRuntimeState, HealScanMode, HealStartRequest, HealStatus,
13+
HealTaskRequest,
1314
};
1415

1516
const HEAL_STOP_SUCCESS_MESSAGE: &str = "Heal operation stopped successfully";
@@ -104,6 +105,10 @@ struct HealStatusOutput {
104105
#[serde(skip_serializing_if = "Option::is_none")]
105106
state: Option<HealRuntimeState>,
106107
#[serde(skip_serializing_if = "Option::is_none")]
108+
cluster_status_complete: Option<bool>,
109+
#[serde(skip_serializing_if = "Option::is_none")]
110+
coverage: Option<BackgroundHealCoverage>,
111+
#[serde(skip_serializing_if = "Option::is_none")]
107112
summary: Option<String>,
108113
#[serde(skip_serializing_if = "Option::is_none")]
109114
detail: Option<String>,
@@ -131,6 +136,8 @@ impl From<&HealStatus> for HealStatusOutput {
131136
heal_id: status.heal_id.clone(),
132137
healing: status.healing,
133138
state: status.state,
139+
cluster_status_complete: status.cluster_status_complete,
140+
coverage: status.coverage.clone(),
134141
summary: status.summary.clone(),
135142
detail: status.detail.clone(),
136143
bucket: status.bucket.clone(),
@@ -153,6 +160,8 @@ impl From<&HealStatus> for HealStatusOutput {
153160
fn has_heal_status_details(status: &HealStatus) -> bool {
154161
status.healing
155162
|| status.state.is_some()
163+
|| status.cluster_status_complete.is_some()
164+
|| status.coverage.is_some()
156165
|| !status.heal_id.is_empty()
157166
|| status.summary.is_some()
158167
|| status.detail.is_some()
@@ -210,13 +219,29 @@ enum HealStatusIndicator {
210219
Date(&'static str),
211220
}
212221

222+
fn cluster_heal_status_is_incomplete(status: &HealStatus) -> bool {
223+
status.state == Some(HealRuntimeState::Degraded)
224+
|| status.cluster_status_complete == Some(false)
225+
|| status.coverage.as_ref().is_some_and(|coverage| {
226+
coverage.unknown.is_some_and(|count| count > 0)
227+
|| !coverage.reasons.is_empty()
228+
|| matches!(
229+
(coverage.responded, coverage.expected),
230+
(Some(responded), Some(expected)) if responded < expected
231+
)
232+
})
233+
}
234+
213235
fn heal_status_indicator(status: &HealStatus) -> HealStatusIndicator {
214236
match status.state {
237+
Some(HealRuntimeState::Active) => HealStatusIndicator::Progress("In Progress"),
238+
Some(HealRuntimeState::Degraded) => HealStatusIndicator::Date("Degraded"),
239+
Some(HealRuntimeState::Unknown) => HealStatusIndicator::Date("Unknown"),
240+
_ if cluster_heal_status_is_incomplete(status) => HealStatusIndicator::Date("Unknown"),
215241
Some(HealRuntimeState::Disabled) => HealStatusIndicator::Date("Disabled"),
216242
Some(HealRuntimeState::Uninitialized) => HealStatusIndicator::Date("Uninitialized"),
217-
Some(HealRuntimeState::Active) => HealStatusIndicator::Progress("In Progress"),
218243
Some(HealRuntimeState::Idle) => HealStatusIndicator::Date("Idle"),
219-
Some(HealRuntimeState::Unknown) | None => match status.summary.as_deref() {
244+
None => match status.summary.as_deref() {
220245
Some("running") => HealStatusIndicator::Progress("In Progress"),
221246
Some("finished") => HealStatusIndicator::Progress("Finished"),
222247
Some("stopped") => HealStatusIndicator::Date("Stopped"),
@@ -287,6 +312,42 @@ fn print_heal_status(status: &HealStatus, formatter: &Formatter) {
287312
));
288313
formatter.println("");
289314

315+
let incomplete = cluster_heal_status_is_incomplete(status);
316+
let unknown = status.state == Some(HealRuntimeState::Unknown);
317+
if incomplete {
318+
formatter
319+
.warning("Cluster heal status is incomplete; unavailable peers may still be healing.");
320+
} else if unknown {
321+
formatter.warning("Heal runtime state is unknown; cluster idleness cannot be confirmed.");
322+
}
323+
324+
// Token responses describe one task, not the coverage of a cluster snapshot.
325+
if status.summary.is_none() && status.heal_id.is_empty() {
326+
let completeness = if incomplete {
327+
"Incomplete"
328+
} else if status.cluster_status_complete == Some(true) {
329+
"Complete"
330+
} else {
331+
"Unknown (not reported by server)"
332+
};
333+
formatter.println(&format!(" Cluster status: {completeness}"));
334+
}
335+
if let Some(coverage) = &status.coverage {
336+
let count = |value: Option<u64>| value.map_or_else(|| "?".to_string(), |n| n.to_string());
337+
formatter.println(&format!(
338+
" Node coverage: {}/{} responded, {} unknown",
339+
count(coverage.responded),
340+
count(coverage.expected),
341+
count(coverage.unknown),
342+
));
343+
if !coverage.reasons.is_empty() {
344+
formatter.println(&format!(
345+
" Reasons: {}",
346+
formatter.sanitize_text(&coverage.reasons.join(", "))
347+
));
348+
}
349+
}
350+
290351
if !status.heal_id.is_empty() {
291352
formatter.println(&format!(" Heal ID: {}", status.heal_id));
292353
}
@@ -336,6 +397,12 @@ fn print_heal_status(status: &HealStatus, formatter: &Formatter) {
336397
if let Some(ref last_update) = status.last_update {
337398
formatter.println(&format!(" Last Update: {}", last_update));
338399
}
400+
} else if incomplete || unknown {
401+
formatter.println(" Cluster idleness cannot be confirmed.");
402+
} else if status.state == Some(HealRuntimeState::Disabled) {
403+
formatter.println(" Heal service is disabled.");
404+
} else if status.state == Some(HealRuntimeState::Uninitialized) {
405+
formatter.println(" Heal service is not initialized.");
339406
} else {
340407
formatter.println(" No active heal operation.");
341408
}
@@ -714,4 +781,22 @@ mod tests {
714781
HealStatusIndicator::Date("Disabled")
715782
));
716783
}
784+
785+
#[test]
786+
fn test_explicit_unknown_overrides_legacy_summary_and_healing() {
787+
for summary in [None, Some("running"), Some("finished"), Some("stopped")] {
788+
for healing in [false, true] {
789+
let status = HealStatus {
790+
state: Some(HealRuntimeState::Unknown),
791+
summary: summary.map(str::to_string),
792+
healing,
793+
..Default::default()
794+
};
795+
assert!(matches!(
796+
heal_status_indicator(&status),
797+
HealStatusIndicator::Date("Unknown")
798+
));
799+
}
800+
}
801+
}
717802
}

0 commit comments

Comments
 (0)