fix: make HeadObject consistent after write completion (#2936)

This commit is contained in:
houseme
2026-05-13 11:27:38 +08:00
committed by GitHub
parent 28c9358482
commit 129cb0f920
3 changed files with 138 additions and 1 deletions
@@ -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<dyn std::error::Error + Send + Sync>> {
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(())
}
+3
View File
@@ -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;
+4 -1
View File
@@ -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]))?;