fix(admin): correct pool lifecycle status mapping (#3823)

This commit is contained in:
cxymds
2026-06-24 17:05:07 +08:00
committed by GitHub
parent eef8f43251
commit 57e12e56d2
4 changed files with 251 additions and 53 deletions
+72 -9
View File
@@ -277,6 +277,9 @@ fn first_resumable_decommission_queue_indices(meta: &PoolMeta) -> Vec<usize> {
let mut indices = Vec::new();
for (idx, pool) in meta.pools.iter().enumerate() {
if let Some(decommission) = &pool.decommission {
if !decommission.has_decommission_state() {
continue;
}
if decommission.complete {
continue;
}
@@ -398,14 +401,14 @@ fn is_decommission_active(complete: bool, failed: bool, canceled: bool) -> bool
pub(crate) fn pool_meta_has_active_decommission(meta: &PoolMeta) -> bool {
meta.pools.iter().any(|pool| {
pool.decommission
.as_ref()
.is_some_and(|info| is_decommission_active(info.complete, info.failed, info.canceled))
pool.decommission.as_ref().is_some_and(|info| {
info.has_decommission_state() && is_decommission_active(info.complete, info.failed, info.canceled)
})
})
}
fn is_decommission_suspended(info: &PoolDecommissionInfo) -> bool {
!info.queued
info.has_decommission_state() && !info.queued
}
fn validate_decommission_terminal_state(complete: bool, failed: bool, canceled: bool) -> Result<()> {
@@ -438,6 +441,9 @@ fn decommission_start_pool_state(pool: Option<&PoolStatus>) -> DecommissionStart
let Some(info) = pool.decommission.as_ref() else {
return DecommissionStartPoolState::Active;
};
if !info.has_decommission_state() {
return DecommissionStartPoolState::Active;
}
if info.complete {
DecommissionStartPoolState::Decommissioned
@@ -1540,7 +1546,7 @@ impl PoolMeta {
let (decommission_present, complete, failed, canceled) = pool
.decommission
.as_ref()
.map(|info| (true, info.complete, info.failed, info.canceled))
.map(|info| (info.has_decommission_state(), info.complete, info.failed, info.canceled))
.unwrap_or((false, false, false, false));
ensure_decommission_clear_allowed(true, decommission_present, complete, failed, canceled)?;
@@ -1806,6 +1812,9 @@ impl PoolMeta {
let mut new_pools = Vec::new();
for pool in &self.pools {
if let Some(decommission) = &pool.decommission {
if !decommission.has_decommission_state() {
continue;
}
if decommission.complete || decommission.failed || decommission.canceled {
// Recovery is not required when:
// - Decommissioning completed
@@ -1878,6 +1887,27 @@ pub struct PoolDecommissionInfo {
}
impl PoolDecommissionInfo {
pub fn has_decommission_state(&self) -> bool {
self.complete
|| self.failed
|| self.canceled
|| self.queued
|| self.start_time.is_some()
|| self.start_size > 0
|| !self.queued_buckets.is_empty()
|| !self.decommissioned_buckets.is_empty()
|| !self.bucket.is_empty()
|| !self.prefix.is_empty()
|| !self.object.is_empty()
|| !self.stage.is_empty()
|| self.items_decommissioned > 0
|| self.items_decommission_failed > 0
|| self.bytes_done > 0
|| self.bytes_failed > 0
|| self.terminal_reload_attempt_at.is_some()
|| !self.terminal_reload_failures.is_empty()
}
fn counted_items(&self) -> usize {
self.items_decommissioned.saturating_add(self.items_decommission_failed)
}
@@ -2215,7 +2245,11 @@ impl ECStore {
let (pool_present, decommission_present, terminal) = if let Some(pool) = lock.pools.get(idx) {
if let Some(info) = pool.decommission.as_ref() {
already_canceled = info.canceled;
(true, true, should_reject_decommission_cancel_as_terminal(info.complete, info.failed))
(
true,
info.has_decommission_state(),
should_reject_decommission_cancel_as_terminal(info.complete, info.failed),
)
} else {
(true, false, false)
}
@@ -2339,6 +2373,7 @@ impl ECStore {
let pool_meta = self.pool_meta.read().await;
for pool in pool_meta.pools.iter() {
if let Some(ref info) = pool.decommission
&& info.has_decommission_state()
&& !info.complete
&& !info.failed
&& !info.canceled
@@ -5101,7 +5136,10 @@ mod pools_tests {
..Default::default()
};
let mut active = previous.clone();
active.pools[0].decommission = Some(PoolDecommissionInfo::default());
active.pools[0].decommission = Some(PoolDecommissionInfo {
start_time: Some(OffsetDateTime::UNIX_EPOCH),
..Default::default()
});
assert!(active.is_suspended(0));
assert_eq!(
@@ -6033,13 +6071,16 @@ mod pools_tests {
}
#[test]
fn test_pool_meta_has_active_decommission_counts_active_and_queued_states() {
fn test_pool_meta_has_active_decommission_counts_running_and_queued_states() {
let active_meta = PoolMeta {
pools: vec![PoolStatus {
id: 0,
cmd_line: "pool-0".to_string(),
last_update: OffsetDateTime::UNIX_EPOCH,
decommission: Some(PoolDecommissionInfo::default()),
decommission: Some(PoolDecommissionInfo {
start_time: Some(OffsetDateTime::UNIX_EPOCH),
..Default::default()
}),
}],
..Default::default()
};
@@ -6060,6 +6101,27 @@ mod pools_tests {
assert!(pool_meta_has_active_decommission(&queued_meta));
}
#[test]
fn test_pool_meta_has_active_decommission_ignores_capacity_placeholder() {
let meta = PoolMeta {
pools: vec![PoolStatus {
id: 0,
cmd_line: "pool-0".to_string(),
last_update: OffsetDateTime::UNIX_EPOCH,
decommission: Some(PoolDecommissionInfo {
total_size: 100,
current_size: 75,
..Default::default()
}),
}],
..Default::default()
};
assert!(!pool_meta_has_active_decommission(&meta));
assert!(!meta.is_suspended(0));
assert_eq!(decommission_start_pool_state(meta.pools.first()), DecommissionStartPoolState::Active);
}
#[test]
fn test_pool_meta_has_active_decommission_ignores_terminal_states() {
let terminal_meta = PoolMeta {
@@ -6158,6 +6220,7 @@ mod pools_tests {
cmd_line: "pool-0".to_string(),
last_update: OffsetDateTime::UNIX_EPOCH,
decommission: Some(PoolDecommissionInfo {
start_time: Some(OffsetDateTime::UNIX_EPOCH),
complete: false,
failed: false,
canceled: false,
+43 -5
View File
@@ -39,6 +39,13 @@ pub(super) fn validate_rebalance_disk_stats_coverage(disk_stats: &[DiskStat]) ->
Ok(())
}
fn pool_rebalance_status_from_meta(meta: Option<&RebalanceMeta>, pool_index: usize) -> (RebalStatus, bool) {
meta.and_then(|meta| meta.pool_stats.get(pool_index))
.filter(|pool_stat| pool_stat.participating)
.map(|pool_stat| (pool_stat.info.status, pool_stat.info.stopping))
.unwrap_or_default()
}
impl ECStore {
pub(super) async fn save_rebalance_meta_with_merge<S>(
&self,
@@ -442,11 +449,7 @@ impl ECStore {
pub async fn pool_rebalance_status(&self, pool_index: usize) -> (RebalStatus, bool) {
let rebalance_meta = self.rebalance_meta.read().await;
rebalance_meta
.as_ref()
.and_then(|meta| meta.pool_stats.get(pool_index))
.map(|pool_stat| (pool_stat.info.status, pool_stat.info.stopping))
.unwrap_or_default()
pool_rebalance_status_from_meta(rebalance_meta.as_ref(), pool_index)
}
pub async fn current_rebalance_id(&self) -> Option<String> {
@@ -530,3 +533,38 @@ impl ECStore {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_rebalance_status_ignores_non_participating_pool_state() {
let meta = RebalanceMeta {
pool_stats: vec![
RebalanceStats {
participating: false,
info: RebalanceInfo {
status: RebalStatus::Started,
stopping: true,
..Default::default()
},
..Default::default()
},
RebalanceStats {
participating: true,
info: RebalanceInfo {
status: RebalStatus::Started,
..Default::default()
},
..Default::default()
},
],
..Default::default()
};
assert_eq!(pool_rebalance_status_from_meta(Some(&meta), 0), (RebalStatus::None, false));
assert_eq!(pool_rebalance_status_from_meta(Some(&meta), 1), (RebalStatus::Started, false));
assert_eq!(pool_rebalance_status_from_meta(Some(&meta), 2), (RebalStatus::None, false));
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ fn pool_meta_has_active_decommission(meta: &PoolMeta) -> bool {
meta.pools.iter().any(|pool| {
pool.decommission
.as_ref()
.is_some_and(|info| !info.complete && !info.failed && !info.canceled)
.is_some_and(|info| info.has_decommission_state() && !info.complete && !info.failed && !info.canceled)
})
}