mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
fix: make HeadObject consistent after write completion (#2936)
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user