fix(heal): track root erasure-set rebuild status (#3625)

This commit is contained in:
Henry Guo
2026-06-19 21:33:58 +08:00
committed by GitHub
parent 68a061c548
commit 2052336a53
3 changed files with 227 additions and 8 deletions
+86 -3
View File
@@ -26,6 +26,7 @@ use hyper::{Method, StatusCode};
use matchit::Params;
use rustfs_common::heal_channel::{HealChannelPriority, HealChannelRequest, HealOpts, HealRequestSource};
use rustfs_config::MAX_HEAL_REQUEST_SIZE;
use rustfs_heal::heal::utils::format_set_disk_id;
use rustfs_policy::policy::action::{Action, AdminAction};
use rustfs_scanner::scanner::{BackgroundHealInfo, read_background_heal_info};
use rustfs_storage_api::HealOperations as _;
@@ -247,7 +248,9 @@ fn encode_heal_task_status(
}
fn build_heal_channel_request(hip: &HealInitParams) -> HealChannelRequest {
let recursive = if !hip.bucket.is_empty() && hip.obj_prefix.is_empty() {
let root_erasure_set_target =
hip.bucket.is_empty() && hip.obj_prefix.is_empty() && matches!((hip.hs.pool, hip.hs.set), (Some(_), Some(_)));
let recursive = if (!hip.bucket.is_empty() && hip.obj_prefix.is_empty()) || root_erasure_set_target {
true
} else {
hip.hs.recursive
@@ -265,6 +268,9 @@ fn build_heal_channel_request(hip: &HealInitParams) -> HealChannelRequest {
heal_request.pool_index = hip.hs.pool;
heal_request.set_index = hip.hs.set;
if root_erasure_set_target && let (Some(pool), Some(set)) = (hip.hs.pool, hip.hs.set) {
heal_request.disk = Some(format_set_disk_id(pool, set));
}
heal_request.scan_mode = Some(hip.hs.scan_mode);
heal_request.remove_corrupted = Some(hip.hs.remove);
heal_request.recreate_missing = Some(hip.hs.recreate);
@@ -335,14 +341,25 @@ fn encode_background_heal_status(
fn validate_heal_request_mode(hip: &HealInitParams) -> S3Result<()> {
if hip.bucket.is_empty() && hip.client_token.is_empty() && !hip.force_stop {
return Err(s3_error!(InvalidRequest, "starting heal without a bucket target is not supported"));
return match (hip.hs.pool, hip.hs.set) {
(Some(_), Some(_)) => Ok(()),
(Some(_), None) | (None, Some(_)) => {
Err(s3_error!(InvalidRequest, "root heal erasure-set target requires both pool and set"))
}
(None, None) => Err(s3_error!(InvalidRequest, "starting heal without a bucket target is not supported")),
};
}
Ok(())
}
fn should_handle_root_heal_directly(hip: &HealInitParams) -> bool {
hip.bucket.is_empty() && hip.obj_prefix.is_empty() && hip.client_token.is_empty() && !hip.force_stop
hip.bucket.is_empty()
&& hip.obj_prefix.is_empty()
&& hip.client_token.is_empty()
&& !hip.force_stop
&& hip.hs.pool.is_none()
&& hip.hs.set.is_none()
}
fn map_root_heal_status(heal_err: Option<crate::admin::storage_compat::Error>) -> S3Result<()> {
@@ -836,6 +853,33 @@ mod tests {
assert_eq!(request.set_index, Some(2));
}
#[test]
fn test_root_heal_channel_request_with_pool_set_targets_erasure_set() {
let hip = HealInitParams {
hs: HealOpts {
scan_mode: HealScanMode::Deep,
recreate: true,
pool: Some(1),
set: Some(2),
..Default::default()
},
force_start: true,
..Default::default()
};
let request = build_heal_channel_request(&hip);
assert_eq!(request.bucket, "");
assert_eq!(request.disk.as_deref(), Some("pool_1_set_2"));
assert_eq!(request.priority, HealChannelPriority::High);
assert_eq!(request.source, HealRequestSource::Admin);
assert_eq!(request.pool_index, Some(1));
assert_eq!(request.set_index, Some(2));
assert_eq!(request.scan_mode, Some(HealScanMode::Deep));
assert_eq!(request.recursive, Some(true));
assert_eq!(request.recreate_missing, Some(true));
}
#[test]
fn test_bucket_heal_channel_request_defaults_to_recursive() {
let hip = HealInitParams {
@@ -895,6 +939,37 @@ mod tests {
);
}
#[test]
fn test_validate_heal_request_mode_allows_root_erasure_set_target() {
validate_heal_request_mode(&HealInitParams {
hs: HealOpts {
pool: Some(1),
set: Some(2),
..Default::default()
},
..Default::default()
})
.expect("root heal with pool and set should start tracked erasure-set rebuild");
}
#[test]
fn test_validate_heal_request_mode_rejects_partial_root_erasure_set_target() {
let err = validate_heal_request_mode(&HealInitParams {
hs: HealOpts {
pool: Some(1),
..Default::default()
},
..Default::default()
})
.expect_err("root heal must provide both pool and set");
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
assert!(
err.to_string()
.contains("root heal erasure-set target requires both pool and set")
);
}
#[test]
fn test_should_handle_root_heal_directly_for_root_start_modes() {
assert!(should_handle_root_heal_directly(&HealInitParams::default()));
@@ -918,6 +993,14 @@ mod tests {
bucket: "bucket".to_string(),
..Default::default()
}));
assert!(!should_handle_root_heal_directly(&HealInitParams {
hs: HealOpts {
pool: Some(1),
set: Some(2),
..Default::default()
},
..Default::default()
}));
}
#[test]