fix(ecstore): harden rebalance data movement (#3234)

* fix(ecstore): harden rebalance data movement

* fix(ecstore): preserve failed rebalance status

* fix(ecstore): avoid rebalance walkdir total timeout

* fix(ecstore): retry rebalance listing timeouts

* fix(ecstore): restrict data movement resume target

* refactor(ecstore): simplify multipart movement target

* fix(ecstore): restore resume target checks

* perf(ecstore): speed up rebalance bucket merges

* fix: keep rebalance listing alive on transient failures

---------

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
cxymds
2026-06-08 18:40:26 +08:00
committed by GitHub
parent 2fc1cf90e0
commit 9504dff595
11 changed files with 2673 additions and 139 deletions
+53 -1
View File
@@ -1136,10 +1136,16 @@ impl DiskAPI for LocalDiskWrapper {
}
async fn walk_dir<W: tokio::io::AsyncWrite + Unpin + Send>(&self, opts: WalkDirOptions, wr: &mut W) -> Result<()> {
let timeout_duration = if opts.skip_total_timeout {
Duration::ZERO
} else {
get_drive_walkdir_timeout()
};
self.track_disk_health_with_op_and_timeout_action(
"walk_dir",
|| async { self.disk.walk_dir(opts, wr).await },
get_drive_walkdir_timeout(),
timeout_duration,
// Listing/scanner backpressure should fail only the current walk, not poison drive health.
TimeoutHealthAction::IgnoreFailure,
)
@@ -1647,6 +1653,52 @@ mod tests {
.await;
}
#[tokio::test]
async fn walk_dir_skip_total_timeout_keeps_stream_pending() {
temp_env::async_with_vars([(rustfs_config::ENV_DRIVE_WALKDIR_TIMEOUT_SECS, Some("1"))], async {
let dir = tempfile::tempdir().expect("temp dir should be created");
let endpoint =
Endpoint::try_from(dir.path().to_str().expect("temp dir should be valid UTF-8")).expect("endpoint should parse");
let disk = Arc::new(LocalDisk::new(&endpoint, false).await.expect("local disk should be created"));
let wrapper = LocalDiskWrapper::new(disk, false);
let bucket = "test-bucket";
let object = "test-object";
wrapper.make_volume(bucket).await.expect("bucket should be created");
let mut file_info = FileInfo::new(&format!("{bucket}/{object}"), 1, 0);
file_info.volume = bucket.to_string();
file_info.name = object.to_string();
file_info.mod_time = Some(::time::OffsetDateTime::now_utc());
file_info.erasure.index = 1;
wrapper
.write_metadata("", bucket, object, file_info)
.await
.expect("object metadata should be written");
let mut writer = PendingWriter;
let result = tokio::time::timeout(
Duration::from_millis(20),
wrapper.walk_dir(
WalkDirOptions {
bucket: bucket.to_string(),
recursive: true,
skip_total_timeout: true,
..Default::default()
},
&mut writer,
),
)
.await;
assert!(result.is_err(), "skip_total_timeout should leave backpressured walk pending");
assert_eq!(wrapper.runtime_state(), RuntimeDriveHealthState::Online);
assert!(!wrapper.health.is_faulty());
})
.await;
}
#[tokio::test]
async fn walk_dir_timeout_does_not_break_followup_stat_volume() {
temp_env::async_with_vars([(rustfs_config::ENV_DRIVE_WALKDIR_TIMEOUT_SECS, Some("1"))], async {
+6
View File
@@ -698,6 +698,10 @@ pub struct WalkDirOptions {
// DiskID contains the disk ID of the disk.
// Leave empty to not check disk ID.
pub disk_id: String,
// Skip the wrapper-level total timeout for long streaming walks.
#[serde(default)]
pub skip_total_timeout: bool,
}
#[derive(Clone, Debug, Default)]
@@ -894,6 +898,7 @@ mod tests {
forward_to: Some("object/path".to_string()),
limit: 100,
disk_id: "disk-123".to_string(),
skip_total_timeout: false,
};
assert_eq!(opts.bucket, "test-bucket");
@@ -904,6 +909,7 @@ mod tests {
assert_eq!(opts.forward_to, Some("object/path".to_string()));
assert_eq!(opts.limit, 100);
assert_eq!(opts.disk_id, "disk-123");
assert!(!opts.skip_total_timeout);
}
/// Test DeleteOptions structure