From 129cb0f9206f5a44d047bb49a4903c2bf30cc324 Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 13 May 2026 11:27:38 +0800 Subject: [PATCH] fix: make HeadObject consistent after write completion (#2936) --- .../src/head_object_consistency_test.rs | 131 ++++++++++++++++++ crates/e2e_test/src/lib.rs | 3 + crates/ecstore/src/set_disk.rs | 5 +- 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 crates/e2e_test/src/head_object_consistency_test.rs diff --git a/crates/e2e_test/src/head_object_consistency_test.rs b/crates/e2e_test/src/head_object_consistency_test.rs new file mode 100644 index 000000000..93e7b8a4e --- /dev/null +++ b/crates/e2e_test/src/head_object_consistency_test.rs @@ -0,0 +1,131 @@ +// Copyright 2026 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::{RustFSTestEnvironment, init_logging, local_http_client}; +use aws_sdk_s3::presigning::PresigningConfig; +use aws_sdk_s3::primitives::ByteStream; +use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart}; +use serial_test::serial; +use std::time::Duration; +use tracing::info; + +const CONSISTENCY_BUCKET: &str = "head-consistency-bucket"; +const PUT_KEY: &str = "consistency-put-object.txt"; +const MPU_KEY: &str = "consistency-multipart-object.txt"; + +fn list_contains_key(output: &aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Output, key: &str) -> bool { + output.contents().iter().any(|obj| obj.key().is_some_and(|k| k == key)) +} + +#[tokio::test] +#[serial] +async fn head_object_consistency_after_write_and_multipart_and_presigned_head() +-> Result<(), Box> { + init_logging(); + info!("Starting HeadObject consistency regression test"); + + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(Vec::new()).await?; + + let client = env.create_s3_client(); + env.create_test_bucket(CONSISTENCY_BUCKET).await?; + + client + .put_object() + .bucket(CONSISTENCY_BUCKET) + .key(PUT_KEY) + .body(ByteStream::from_static(b"head-consistency-put-body")) + .send() + .await?; + + client.get_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?; + let put_list = client + .list_objects_v2() + .bucket(CONSISTENCY_BUCKET) + .prefix(PUT_KEY) + .send() + .await?; + assert!(list_contains_key(&put_list, PUT_KEY), "ListObjectsV2 should include the PutObject key"); + + client.head_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?; + + let create = client + .create_multipart_upload() + .bucket(CONSISTENCY_BUCKET) + .key(MPU_KEY) + .send() + .await?; + let upload_id = create.upload_id().ok_or("missing multipart upload id")?.to_string(); + + let part1 = client + .upload_part() + .bucket(CONSISTENCY_BUCKET) + .key(MPU_KEY) + .upload_id(&upload_id) + .part_number(1) + .body(ByteStream::from_static(b"head-consistency-multipart-part-1")) + .send() + .await?; + let completed = CompletedMultipartUpload::builder() + .parts( + CompletedPart::builder() + .part_number(1) + .set_e_tag(part1.e_tag().map(str::to_string)) + .build(), + ) + .build(); + + client + .complete_multipart_upload() + .bucket(CONSISTENCY_BUCKET) + .key(MPU_KEY) + .upload_id(&upload_id) + .multipart_upload(completed) + .send() + .await?; + + client.get_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?; + let mpu_list = client + .list_objects_v2() + .bucket(CONSISTENCY_BUCKET) + .prefix(MPU_KEY) + .send() + .await?; + assert!( + list_contains_key(&mpu_list, MPU_KEY), + "ListObjectsV2 should include the completed multipart key" + ); + + client.head_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?; + + let presigned = client + .head_object() + .bucket(CONSISTENCY_BUCKET) + .key(PUT_KEY) + .presigned(PresigningConfig::expires_in(Duration::from_secs(300))?) + .await?; + let presigned_resp = local_http_client().head(presigned.uri().to_string()).send().await?; + assert!( + presigned_resp.status().is_success(), + "Presigned HEAD should succeed, got status {}", + presigned_resp.status() + ); + + client.delete_object().bucket(CONSISTENCY_BUCKET).key(PUT_KEY).send().await?; + client.delete_object().bucket(CONSISTENCY_BUCKET).key(MPU_KEY).send().await?; + env.delete_test_bucket(CONSISTENCY_BUCKET).await?; + env.stop_server(); + + Ok(()) +} diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index f855c4486..39c770260 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -111,6 +111,9 @@ mod group_delete_test; #[cfg(test)] mod head_object_range_test; +#[cfg(test)] +mod head_object_consistency_test; + #[cfg(test)] mod copy_object_metadata_test; diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index c139270e2..b3466d016 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -1937,8 +1937,11 @@ impl ObjectOperations for SetDisks { None }; + // Use the same full xl.meta read path as GetObject metadata resolution. + // This avoids HEAD/GetObject metadata visibility skew immediately after + // PutObject/CompleteMultipartUpload. let (fi, _, _) = self - .get_object_fileinfo(bucket, object, opts, false) + .get_object_fileinfo(bucket, object, opts, true) .await .map_err(|e| to_object_err(e, vec![bucket, object]))?;