Skip to content

Commit 7432940

Browse files
committed
feat(admin): expose rebalance cleanup warnings
1 parent 0e1f3bf commit 7432940

6 files changed

Lines changed: 176 additions & 8 deletions

File tree

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::Serialize;
66
use super::get_admin_client;
77
use crate::exit_code::ExitCode;
88
use crate::output::Formatter;
9-
use rc_core::admin::{AdminApi, RebalancePoolStatus, RebalanceStatus};
9+
use rc_core::admin::{AdminApi, RebalanceCleanupWarnings, RebalancePoolStatus, RebalanceStatus};
1010

1111
/// Rebalance subcommands
1212
#[derive(Subcommand, Debug)]
@@ -217,6 +217,14 @@ fn print_rebalance_pool(pool: &RebalancePoolStatus, formatter: &Formatter) {
217217
formatter.println(&format!(" Last error: {error}"));
218218
}
219219

220+
if pool.cleanup_warnings.count > 0 {
221+
formatter.println(&format!(
222+
" {} {}",
223+
formatter.theme().warning.apply_to("Cleanup warnings:"),
224+
format_cleanup_warnings(&pool.cleanup_warnings)
225+
));
226+
}
227+
220228
if let Some(progress) = &pool.progress {
221229
formatter.println(&format!(
222230
" Progress: {} moved, {} objects, {} versions",
@@ -242,6 +250,41 @@ fn print_rebalance_pool(pool: &RebalancePoolStatus, formatter: &Formatter) {
242250
}
243251
}
244252

253+
fn format_cleanup_warnings(warnings: &RebalanceCleanupWarnings) -> String {
254+
let mut details = vec![format!("{} warning(s)", warnings.count)];
255+
256+
if let Some(message) = warnings
257+
.last_message
258+
.as_deref()
259+
.filter(|value| !value.is_empty())
260+
{
261+
details.push(format!("last message: {message}"));
262+
}
263+
if let Some(bucket) = warnings
264+
.last_bucket
265+
.as_deref()
266+
.filter(|value| !value.is_empty())
267+
{
268+
details.push(format!("bucket: {bucket}"));
269+
}
270+
if let Some(object) = warnings
271+
.last_object
272+
.as_deref()
273+
.filter(|value| !value.is_empty())
274+
{
275+
details.push(format!("object: {object}"));
276+
}
277+
if let Some(at) = warnings
278+
.last_at
279+
.as_deref()
280+
.filter(|value| !value.is_empty())
281+
{
282+
details.push(format!("at: {at}"));
283+
}
284+
285+
details.join(", ")
286+
}
287+
245288
fn style_status(status: &str, formatter: &Formatter) -> String {
246289
match status {
247290
"Started" | "running" => formatter.style_name(status),
@@ -326,4 +369,20 @@ mod tests {
326369
assert_eq!(style_status("idle", &formatter), "idle");
327370
assert_eq!(style_status("Queued", &formatter), "Queued");
328371
}
372+
373+
#[test]
374+
fn test_format_cleanup_warnings() {
375+
let warnings = RebalanceCleanupWarnings {
376+
count: 2,
377+
last_message: Some("cleanup warning".to_string()),
378+
last_bucket: Some("bucket-a".to_string()),
379+
last_object: Some("object-a".to_string()),
380+
last_at: Some("2026-06-12T00:00:00Z".to_string()),
381+
};
382+
383+
assert_eq!(
384+
format_cleanup_warnings(&warnings),
385+
"2 warning(s), last message: cleanup warning, bucket: bucket-a, object: object-a, at: 2026-06-12T00:00:00Z"
386+
);
387+
}
329388
}

crates/cli/tests/admin_expand.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fn scale_start_dispatches_to_rebalance_start_with_expansion_json() {
4848
fn expand_status_dispatches_to_rebalance_status_json() {
4949
let config_dir = tempfile::tempdir().expect("create config dir");
5050
let (endpoint, receiver, handle) = start_admin_test_server(
51-
r#"{"id":"rebalance-123","pools":[],"stoppedAt":"2026-05-06T00:00:00Z"}"#,
51+
r#"{"id":"rebalance-123","pools":[{"id":0,"status":"Completed","used":0.5,"lastError":null,"cleanupWarnings":{"count":1,"lastMsg":"cleanup warning","lastBucket":"test-bucket","lastObject":"object-a","lastAt":"2026-06-12T00:00:00Z"},"progress":null}],"stoppedAt":"2026-05-06T00:00:00Z"}"#,
5252
);
5353

5454
let output = Command::new(rc_binary())
@@ -73,7 +73,12 @@ fn expand_status_dispatches_to_rebalance_status_json() {
7373
.as_array()
7474
.expect("pools should be an array")
7575
.len(),
76-
0
76+
1
77+
);
78+
assert_eq!(payload["pools"][0]["cleanupWarnings"]["count"], 1);
79+
assert_eq!(
80+
payload["pools"][0]["cleanupWarnings"]["lastMsg"],
81+
"cleanup warning"
7782
);
7883

7984
let request = receiver

crates/cli/tests/admin_rebalance.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fn rebalance_start_dispatches_to_rebalance_start_json() {
4545
fn rebalance_status_dispatches_to_rebalance_status_json() {
4646
let config_dir = tempfile::tempdir().expect("create config dir");
4747
let (endpoint, receiver, handle) = start_admin_test_server(
48-
r#"{"id":"rebalance-123","pools":[],"stoppedAt":"2026-05-07T00:00:00Z"}"#,
48+
r#"{"id":"rebalance-123","pools":[{"id":0,"status":"Completed","used":0.5,"lastError":null,"cleanupWarnings":{"count":1,"lastMsg":"cleanup warning","lastBucket":"test-bucket","lastObject":"object-a","lastAt":"2026-06-12T00:00:00Z"},"progress":null}],"stoppedAt":"2026-05-07T00:00:00Z"}"#,
4949
);
5050

5151
let output = Command::new(rc_binary())
@@ -65,7 +65,16 @@ fn rebalance_status_dispatches_to_rebalance_status_json() {
6565
let payload: serde_json::Value = serde_json::from_str(&stdout).expect("JSON output");
6666
assert_eq!(payload["id"], "rebalance-123");
6767
assert_eq!(payload["stoppedAt"], "2026-05-07T00:00:00Z");
68-
assert_eq!(payload["pools"].as_array().expect("pools array").len(), 0);
68+
assert_eq!(payload["pools"].as_array().expect("pools array").len(), 1);
69+
assert_eq!(payload["pools"][0]["cleanupWarnings"]["count"], 1);
70+
assert_eq!(
71+
payload["pools"][0]["cleanupWarnings"]["lastMsg"],
72+
"cleanup warning"
73+
);
74+
assert_eq!(
75+
payload["pools"][0]["cleanupWarnings"]["lastBucket"],
76+
"test-bucket"
77+
);
6978

7079
let request = receiver
7180
.recv_timeout(Duration::from_secs(5))

crates/core/src/admin/cluster.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,11 +730,39 @@ pub struct RebalancePoolStatus {
730730
#[serde(default, rename = "lastError")]
731731
pub last_error: Option<String>,
732732

733+
/// Cleanup warnings observed after this pool finishes rebalance.
734+
#[serde(default, rename = "cleanupWarnings")]
735+
pub cleanup_warnings: RebalanceCleanupWarnings,
736+
733737
/// Rebalance progress, if this pool is active.
734738
#[serde(default)]
735739
pub progress: Option<RebalancePoolProgress>,
736740
}
737741

742+
/// Cleanup warnings recorded for a rebalanced pool.
743+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
744+
pub struct RebalanceCleanupWarnings {
745+
/// Number of cleanup warnings observed.
746+
#[serde(default)]
747+
pub count: u64,
748+
749+
/// Last cleanup warning message.
750+
#[serde(default, rename = "lastMsg")]
751+
pub last_message: Option<String>,
752+
753+
/// Bucket associated with the last cleanup warning.
754+
#[serde(default, rename = "lastBucket")]
755+
pub last_bucket: Option<String>,
756+
757+
/// Object associated with the last cleanup warning.
758+
#[serde(default, rename = "lastObject")]
759+
pub last_object: Option<String>,
760+
761+
/// Timestamp of the last cleanup warning.
762+
#[serde(default, rename = "lastAt")]
763+
pub last_at: Option<String>,
764+
}
765+
738766
/// Rebalance progress for a single pool.
739767
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
740768
pub struct RebalancePoolProgress {
@@ -919,13 +947,18 @@ mod tests {
919947

920948
#[test]
921949
fn test_rebalance_status_deserialization() {
922-
let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Started","used":0.5,"lastError":null,"progress":{"objects":3,"versions":4,"bytes":1024,"remainingBuckets":2,"bucket":"bucket","object":"object","elapsed":10,"eta":20}}],"stoppedAt":null}"#;
950+
let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Started","used":0.5,"lastError":null,"cleanupWarnings":{"count":1,"lastMsg":"cleanup warning","lastBucket":"bucket","lastObject":"object","lastAt":"2026-06-12T00:00:00Z"},"progress":{"objects":3,"versions":4,"bytes":1024,"remainingBuckets":2,"bucket":"bucket","object":"object","elapsed":10,"eta":20}}],"stoppedAt":null}"#;
923951

924952
let status: RebalanceStatus = serde_json::from_str(json).unwrap();
925953

926954
assert_eq!(status.id, "rebalance-1");
927955
assert_eq!(status.pools.len(), 1);
928956
assert_eq!(status.pools[0].used, 0.5);
957+
assert_eq!(status.pools[0].cleanup_warnings.count, 1);
958+
assert_eq!(
959+
status.pools[0].cleanup_warnings.last_message.as_deref(),
960+
Some("cleanup warning")
961+
);
929962
let progress = status.pools[0]
930963
.progress
931964
.as_ref()
@@ -934,6 +967,16 @@ mod tests {
934967
assert_eq!(progress.remaining_buckets, 2);
935968
}
936969

970+
#[test]
971+
fn test_rebalance_status_defaults_cleanup_warnings() {
972+
let json = r#"{"id":"rebalance-1","pools":[{"id":0,"status":"Completed","used":0.5,"lastError":null,"progress":null}],"stoppedAt":null}"#;
973+
974+
let status: RebalanceStatus = serde_json::from_str(json).unwrap();
975+
976+
assert_eq!(status.pools[0].cleanup_warnings.count, 0);
977+
assert_eq!(status.pools[0].cleanup_warnings.last_message, None);
978+
}
979+
937980
#[test]
938981
fn test_serialization() {
939982
let info = ClusterInfo {

crates/core/src/admin/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ pub use cluster::{
1111
BackendInfo, BackendType, BucketsInfo, ClusterInfo, DiskInfo, HealDriveInfo, HealDriveInfos,
1212
HealResultItem, HealScanMode, HealStartRequest, HealStatus, HealingDiskInfo, MemStats,
1313
ObjectsInfo, PoolDecommissionInfo, PoolErasureSetInfo, PoolStatus, PoolTarget,
14-
RebalancePoolProgress, RebalancePoolStatus, RebalanceStartResult, RebalanceStatus, ServerInfo,
15-
UsageInfo,
14+
RebalanceCleanupWarnings, RebalancePoolProgress, RebalancePoolStatus, RebalanceStartResult,
15+
RebalanceStatus, ServerInfo, UsageInfo,
1616
};
1717
pub use tier::{
1818
TierAliyun, TierAzure, TierConfig, TierCreds, TierGCS, TierHuaweicloud, TierMinIO, TierR2,

schemas/output_v2.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,13 +435,62 @@
435435
}
436436
}
437437
},
438+
"rebalanceCleanupWarnings": {
439+
"type": "object",
440+
"required": [
441+
"count",
442+
"lastMsg",
443+
"lastBucket",
444+
"lastObject",
445+
"lastAt"
446+
],
447+
"properties": {
448+
"count": {
449+
"type": "integer",
450+
"description": "Cleanup warning count"
451+
},
452+
"lastMsg": {
453+
"type": [
454+
"string",
455+
"null"
456+
],
457+
"description": "Last cleanup warning message"
458+
},
459+
"lastBucket": {
460+
"type": [
461+
"string",
462+
"null"
463+
],
464+
"description": "Bucket from the last cleanup warning"
465+
},
466+
"lastObject": {
467+
"type": [
468+
"string",
469+
"null"
470+
],
471+
"description": "Object from the last cleanup warning"
472+
},
473+
"lastAt": {
474+
"oneOf": [
475+
{
476+
"$ref": "#/definitions/timestamp"
477+
},
478+
{
479+
"type": "null"
480+
}
481+
],
482+
"description": "Timestamp of the last cleanup warning"
483+
}
484+
}
485+
},
438486
"rebalancePoolStatus": {
439487
"type": "object",
440488
"required": [
441489
"id",
442490
"status",
443491
"used",
444492
"lastError",
493+
"cleanupWarnings",
445494
"progress"
446495
],
447496
"properties": {
@@ -464,6 +513,9 @@
464513
],
465514
"description": "Last rebalance error for this pool"
466515
},
516+
"cleanupWarnings": {
517+
"$ref": "#/definitions/rebalanceCleanupWarnings"
518+
},
467519
"progress": {
468520
"oneOf": [
469521
{

0 commit comments

Comments
 (0)