fix(ecstore): harden issue3031 multipart validation path (#3106)

* fix(ecstore): harden issue3031 multipart validation path

- clear stale multipart part destinations before rename fan-out
- add repeated part overwrite regression coverage
- reduce remote disk startup false-fault escalation to suspect-first
- refine remote locker diagnostics and lower scanner leader-lock log noise
- add a dedicated 4-node issue3031 docker validation script

* refactor(admin): inline console version json macro

- drop the unused serde_json::json import in admin console
- call serde_json::json! inline in version_handler
- keep the console version response behavior unchanged

* fix(remote-disk): recover suspect health on probe success

- record probe success during remote disk health checks so suspect drives recover
- use async_with_vars for the remote disk health probe test
- make the missing-listener test assert the state transition more robustly
This commit is contained in:
houseme
2026-05-28 22:26:31 +08:00
committed by GitHub
parent 8d20e89bf8
commit 088c4bda43
7 changed files with 717 additions and 37 deletions
@@ -3187,6 +3187,76 @@ mod tests {
assert!(!parts.user_defined.contains_key(RUSTFS_MULTIPART_OBJECT_KEY));
}
#[tokio::test]
#[serial]
async fn repeated_upload_part_overwrites_previous_part_state() {
let (_paths, ecstore) = setup_test_env().await;
let bucket = format!("multipart-overwrite-{}", Uuid::new_v4().simple());
let object = "overwrite/object.txt";
create_test_bucket(&ecstore, &bucket).await;
let upload = ecstore
.new_multipart_upload(&bucket, object, &ObjectOptions::default())
.await
.expect("multipart upload should be created");
let mut first = PutObjReader::from_vec(vec![1, 2, 3]);
let first_part = ecstore
.put_object_part(&bucket, object, &upload.upload_id, 1, &mut first, &ObjectOptions::default())
.await
.expect("first multipart part should be uploaded");
let mut second = PutObjReader::from_vec(vec![4, 5, 6, 7]);
let second_part = ecstore
.put_object_part(&bucket, object, &upload.upload_id, 1, &mut second, &ObjectOptions::default())
.await
.expect("second multipart part should overwrite the previous part");
assert_ne!(
first_part.etag, second_part.etag,
"the overwrite path should persist the latest part metadata rather than reusing stale state"
);
let parts = ecstore
.list_object_parts(
&bucket,
object,
&upload.upload_id,
None,
crate::set_disk::MAX_PARTS_COUNT,
&ObjectOptions::default(),
)
.await
.expect("multipart parts should be readable after overwrite");
assert_eq!(parts.parts.len(), 1, "only the latest version of part 1 should remain visible");
assert_eq!(parts.parts[0].part_num, 1);
assert_eq!(parts.parts[0].etag, second_part.etag);
assert_eq!(parts.parts[0].size, second_part.size);
assert_eq!(parts.parts[0].actual_size, second_part.actual_size);
let completed = ecstore
.complete_multipart_upload(
&bucket,
object,
&upload.upload_id,
vec![crate::store_api::CompletePart {
part_num: 1,
etag: second_part.etag.clone(),
checksum_crc32: None,
checksum_crc32c: None,
checksum_sha1: None,
checksum_sha256: None,
checksum_crc64nvme: None,
}],
&ObjectOptions::default(),
)
.await
.expect("complete multipart upload should succeed with the latest overwritten part");
assert_eq!(completed.size, second_part.size as i64);
}
#[tokio::test]
#[serial]
async fn cleanup_removes_empty_multipart_sha_dirs() {