mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 16:37:07 +00:00
fix(ecstore): precheck decommission capacity (#4196)
This commit is contained in:
@@ -89,6 +89,7 @@ const DECOMMISSION_PROGRESS_SAVE_INTERVAL: Duration = Duration::seconds(30);
|
|||||||
const DECOMMISSION_PROGRESS_SAVE_ITEM_THRESHOLD: usize = 1000;
|
const DECOMMISSION_PROGRESS_SAVE_ITEM_THRESHOLD: usize = 1000;
|
||||||
const DECOMMISSION_BUCKET_CONCURRENCY_ENV: &str = "RUSTFS_DECOMMISSION_BUCKET_CONCURRENCY";
|
const DECOMMISSION_BUCKET_CONCURRENCY_ENV: &str = "RUSTFS_DECOMMISSION_BUCKET_CONCURRENCY";
|
||||||
const DECOMMISSION_BUCKET_CONCURRENCY_DEFAULT_CAP: usize = 4;
|
const DECOMMISSION_BUCKET_CONCURRENCY_DEFAULT_CAP: usize = 4;
|
||||||
|
const DECOMMISSION_TARGET_CAPACITY_OVERHEAD_PERCENT: usize = 30;
|
||||||
|
|
||||||
pub const POOL_META_NAME: &str = "pool.bin";
|
pub const POOL_META_NAME: &str = "pool.bin";
|
||||||
pub const POOL_META_FORMAT: u16 = 1;
|
pub const POOL_META_FORMAT: u16 = 1;
|
||||||
@@ -499,6 +500,38 @@ fn ensure_decommission_start_pool_states(meta: &PoolMeta, indices: &[usize]) ->
|
|||||||
ensure_decommission_start_keeps_active_pool(meta, indices)
|
ensure_decommission_start_keeps_active_pool(meta, indices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decommission_target_capacity_required(source_used: usize) -> usize {
|
||||||
|
source_used
|
||||||
|
.saturating_mul(100 + DECOMMISSION_TARGET_CAPACITY_OVERHEAD_PERCENT)
|
||||||
|
.div_ceil(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_decommission_start_target_capacity(
|
||||||
|
meta: &PoolMeta,
|
||||||
|
indices: &[usize],
|
||||||
|
space_infos: &[(usize, PoolSpaceInfo)],
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut source_used = 0usize;
|
||||||
|
let mut target_free = 0usize;
|
||||||
|
|
||||||
|
for (idx, info) in space_infos {
|
||||||
|
if indices.contains(idx) {
|
||||||
|
source_used = source_used.saturating_add(info.used);
|
||||||
|
} else if meta.pools.get(*idx).is_some_and(is_decommission_start_active_pool) {
|
||||||
|
target_free = target_free.saturating_add(info.free);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let required = decommission_target_capacity_required(source_used);
|
||||||
|
if target_free < required {
|
||||||
|
return Err(Error::other(format!(
|
||||||
|
"failed to start decommission: insufficient target pool capacity: required {required} bytes available {target_free} bytes for {source_used} bytes used in decommission pools with {DECOMMISSION_TARGET_CAPACITY_OVERHEAD_PERCENT}% overhead"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_valid_decommission_pool_index(pool_count: usize, idx: usize) -> Result<()> {
|
fn ensure_valid_decommission_pool_index(pool_count: usize, idx: usize) -> Result<()> {
|
||||||
if idx >= pool_count {
|
if idx >= pool_count {
|
||||||
return Err(invalid_decommission_pool_index_error(pool_count, idx));
|
return Err(invalid_decommission_pool_index_error(pool_count, idx));
|
||||||
@@ -2035,7 +2068,7 @@ impl PoolDecommissionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PoolSpaceInfo {
|
pub struct PoolSpaceInfo {
|
||||||
pub free: usize,
|
pub free: usize,
|
||||||
pub total: usize,
|
pub total: usize,
|
||||||
@@ -2322,6 +2355,14 @@ impl ECStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_decommission_all_pool_space_infos(&self) -> Result<Vec<(usize, PoolSpaceInfo)>> {
|
||||||
|
let mut space_infos = Vec::with_capacity(self.pools.len());
|
||||||
|
for idx in 0..self.pools.len() {
|
||||||
|
space_infos.push((idx, self.get_decommission_pool_space_info(idx).await?));
|
||||||
|
}
|
||||||
|
Ok(space_infos)
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn decommission_cancel(&self, idx: usize) -> Result<()> {
|
pub async fn decommission_cancel(&self, idx: usize) -> Result<()> {
|
||||||
ensure_decommission_terminal_operation_supported(self.single_pool(), "cancel decommission")?;
|
ensure_decommission_terminal_operation_supported(self.single_pool(), "cancel decommission")?;
|
||||||
@@ -3798,15 +3839,23 @@ impl ECStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut space_infos = Vec::with_capacity(indices.len());
|
|
||||||
for idx in indices.iter().copied() {
|
|
||||||
let pi = self.get_decommission_pool_space_info(idx).await?;
|
|
||||||
space_infos.push((idx, pi));
|
|
||||||
}
|
|
||||||
|
|
||||||
let _start_guard = self.start_gate.lock().await;
|
let _start_guard = self.start_gate.lock().await;
|
||||||
self.ensure_decommission_rebalance_idle_after_refresh().await?;
|
self.ensure_decommission_rebalance_idle_after_refresh().await?;
|
||||||
|
|
||||||
|
let all_space_infos = self.get_decommission_all_pool_space_infos().await?;
|
||||||
|
{
|
||||||
|
let pool_meta = self.pool_meta.read().await;
|
||||||
|
ensure_decommission_start_pool_states(&pool_meta, &indices)?;
|
||||||
|
ensure_decommission_start_target_capacity(&pool_meta, &indices, &all_space_infos)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut space_infos = Vec::with_capacity(indices.len());
|
||||||
|
for (idx, pi) in all_space_infos.iter().copied() {
|
||||||
|
if indices.contains(&idx) {
|
||||||
|
space_infos.push((idx, pi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let previous_pool_meta = self
|
let previous_pool_meta = self
|
||||||
.save_current_pool_meta_for_decommission_start(&indices, space_infos, decom_buckets)
|
.save_current_pool_meta_for_decommission_start(&indices, space_infos, decom_buckets)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -4900,9 +4949,10 @@ mod pools_tests {
|
|||||||
ensure_decommission_cancel_allowed, ensure_decommission_clear_allowed, ensure_decommission_listing_disks_available,
|
ensure_decommission_cancel_allowed, ensure_decommission_clear_allowed, ensure_decommission_listing_disks_available,
|
||||||
ensure_decommission_not_rebalancing, ensure_decommission_start_allowed, ensure_decommission_start_keeps_active_pool,
|
ensure_decommission_not_rebalancing, ensure_decommission_start_allowed, ensure_decommission_start_keeps_active_pool,
|
||||||
ensure_decommission_start_local_leader, ensure_decommission_start_pool_states,
|
ensure_decommission_start_local_leader, ensure_decommission_start_pool_states,
|
||||||
ensure_decommission_start_rebalance_meta_allowed, ensure_decommission_terminal_operation_supported,
|
ensure_decommission_start_rebalance_meta_allowed, ensure_decommission_start_target_capacity,
|
||||||
ensure_local_decommission_pool_leaders, ensure_valid_decommission_pool_index, first_resumable_decommission_queue_indices,
|
ensure_decommission_terminal_operation_supported, ensure_local_decommission_pool_leaders,
|
||||||
get_by_index, has_active_decommission_canceler, is_decommission_active, is_decommission_cancel_requested,
|
ensure_valid_decommission_pool_index, first_resumable_decommission_queue_indices, get_by_index,
|
||||||
|
has_active_decommission_canceler, is_decommission_active, is_decommission_cancel_requested,
|
||||||
load_decommission_entry_versions, local_decommission_queue_prefix, mark_decommission_bucket_done,
|
load_decommission_entry_versions, local_decommission_queue_prefix, mark_decommission_bucket_done,
|
||||||
merge_pool_status_refresh, missing_decommission_worker_prefix, observe_decommission_terminal_reload_result,
|
merge_pool_status_refresh, missing_decommission_worker_prefix, observe_decommission_terminal_reload_result,
|
||||||
pool_meta_has_active_decommission, require_decommission_store, resolve_decommission_bucket_done_save_result,
|
pool_meta_has_active_decommission, require_decommission_store, resolve_decommission_bucket_done_save_result,
|
||||||
@@ -6517,6 +6567,115 @@ mod pools_tests {
|
|||||||
assert!(err.to_string().contains("at least one active pool must remain"));
|
assert!(err.to_string().contains("at least one active pool must remain"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_decommission_start_target_capacity_allows_sufficient_free_space() {
|
||||||
|
let meta = PoolMeta {
|
||||||
|
pools: vec![decommission_test_pool_status(0, None), decommission_test_pool_status(1, None)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let space_infos = vec![
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 100,
|
||||||
|
total: 1_000,
|
||||||
|
used: 900,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 1_170,
|
||||||
|
total: 2_000,
|
||||||
|
used: 830,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(ensure_decommission_start_target_capacity(&meta, &[0], &space_infos).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_decommission_start_target_capacity_rejects_insufficient_free_space() {
|
||||||
|
let meta = PoolMeta {
|
||||||
|
pools: vec![decommission_test_pool_status(0, None), decommission_test_pool_status(1, None)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let space_infos = vec![
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 100,
|
||||||
|
total: 1_000,
|
||||||
|
used: 900,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 1_169,
|
||||||
|
total: 2_000,
|
||||||
|
used: 831,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let err = ensure_decommission_start_target_capacity(&meta, &[0], &space_infos)
|
||||||
|
.expect_err("target free capacity below 130% of source used should be rejected");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("insufficient target pool capacity"));
|
||||||
|
assert!(err.to_string().contains("required 1170 bytes available 1169 bytes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_decommission_start_target_capacity_ignores_non_active_target_pool() {
|
||||||
|
let meta = PoolMeta {
|
||||||
|
pools: vec![
|
||||||
|
decommission_test_pool_status(0, None),
|
||||||
|
decommission_test_pool_status(
|
||||||
|
1,
|
||||||
|
Some(PoolDecommissionInfo {
|
||||||
|
complete: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
decommission_test_pool_status(2, None),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let space_infos = vec![
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 100,
|
||||||
|
total: 1_000,
|
||||||
|
used: 900,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 10_000,
|
||||||
|
total: 10_000,
|
||||||
|
used: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
PoolSpaceInfo {
|
||||||
|
free: 1_169,
|
||||||
|
total: 2_000,
|
||||||
|
used: 831,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let err = ensure_decommission_start_target_capacity(&meta, &[0], &space_infos)
|
||||||
|
.expect_err("completed pools must not contribute target free capacity");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("required 1170 bytes available 1169 bytes"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ensure_decommission_start_pool_states_rejects_blocked_pool() {
|
fn test_ensure_decommission_start_pool_states_rejects_blocked_pool() {
|
||||||
let meta = PoolMeta {
|
let meta = PoolMeta {
|
||||||
|
|||||||
Reference in New Issue
Block a user