mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 07:57:01 +00:00
fix(ecstore): invalidate xl.meta cache after writes and rename (#2255)
This commit is contained in:
@@ -66,6 +66,11 @@ mod compression_test;
|
|||||||
// Regression test for Issue #1878: DeleteMarkers not visible immediately after delete_objects
|
// Regression test for Issue #1878: DeleteMarkers not visible immediately after delete_objects
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod delete_objects_versioning_test;
|
mod delete_objects_versioning_test;
|
||||||
|
|
||||||
|
// Regression test for Issue #2252: ListObjectVersions misses newest version after put -> delete -> put
|
||||||
|
#[cfg(test)]
|
||||||
|
mod list_object_versions_regression_test;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod protocols;
|
mod protocols;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2024 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.
|
||||||
|
|
||||||
|
//! Regression test for Issue #2252:
|
||||||
|
//! "ListObjectVersions misses the newest version after create -> delete -> create."
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||||
|
use aws_sdk_s3::Client;
|
||||||
|
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
||||||
|
use serial_test::serial;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
fn create_s3_client(env: &RustFSTestEnvironment) -> Client {
|
||||||
|
env.create_s3_client()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_list_object_versions_immediately_returns_latest_put_after_delete_marker() {
|
||||||
|
init_logging();
|
||||||
|
info!("🧪 TEST: ListObjectVersions returns the newest version immediately after put -> delete -> put");
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
||||||
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
||||||
|
|
||||||
|
let client = create_s3_client(&env);
|
||||||
|
let bucket = "test-list-object-versions-2252";
|
||||||
|
let key = "test-prefix/test-object.txt";
|
||||||
|
|
||||||
|
client
|
||||||
|
.create_bucket()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bucket");
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_bucket_versioning()
|
||||||
|
.bucket(bucket)
|
||||||
|
.versioning_configuration(
|
||||||
|
VersioningConfiguration::builder()
|
||||||
|
.status(BucketVersioningStatus::Enabled)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to enable versioning");
|
||||||
|
|
||||||
|
let first_put = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"first version"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to put first object version");
|
||||||
|
let first_version_id = first_put
|
||||||
|
.version_id()
|
||||||
|
.map(str::to_string)
|
||||||
|
.expect("First put should return a version_id");
|
||||||
|
|
||||||
|
let delete_resp = client
|
||||||
|
.delete_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create delete marker");
|
||||||
|
let delete_marker_version_id = delete_resp
|
||||||
|
.version_id()
|
||||||
|
.map(str::to_string)
|
||||||
|
.expect("DeleteObject should return a delete marker version_id");
|
||||||
|
|
||||||
|
let second_put = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(aws_sdk_s3::primitives::ByteStream::from_static(b"second version"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to put second object version");
|
||||||
|
let second_version_id = second_put
|
||||||
|
.version_id()
|
||||||
|
.map(str::to_string)
|
||||||
|
.expect("Second put should return a version_id");
|
||||||
|
|
||||||
|
let first_listing = client
|
||||||
|
.list_object_versions()
|
||||||
|
.bucket(bucket)
|
||||||
|
.prefix(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to list object versions immediately after second put");
|
||||||
|
let second_listing = client
|
||||||
|
.list_object_versions()
|
||||||
|
.bucket(bucket)
|
||||||
|
.prefix(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to list object versions a second time");
|
||||||
|
|
||||||
|
let first_versions = first_listing.versions().to_vec();
|
||||||
|
let first_delete_markers = first_listing.delete_markers().to_vec();
|
||||||
|
let second_versions = second_listing.versions().to_vec();
|
||||||
|
let second_delete_markers = second_listing.delete_markers().to_vec();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"First listing: {} versions, {} delete markers",
|
||||||
|
first_versions.len(),
|
||||||
|
first_delete_markers.len()
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Second listing: {} versions, {} delete markers",
|
||||||
|
second_versions.len(),
|
||||||
|
second_delete_markers.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
first_versions.len(),
|
||||||
|
2,
|
||||||
|
"First ListObjectVersions call should return both object versions immediately (regression #2252)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
first_delete_markers.len(),
|
||||||
|
1,
|
||||||
|
"First ListObjectVersions call should return the delete marker immediately (regression #2252)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second_versions.len(),
|
||||||
|
2,
|
||||||
|
"Second ListObjectVersions call should still return both object versions"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second_delete_markers.len(),
|
||||||
|
1,
|
||||||
|
"Second ListObjectVersions call should still return the delete marker"
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_latest_version = first_versions
|
||||||
|
.iter()
|
||||||
|
.find(|version| version.version_id() == Some(second_version_id.as_str()))
|
||||||
|
.expect("First listing should include the newest object version");
|
||||||
|
assert_eq!(
|
||||||
|
first_latest_version.is_latest(),
|
||||||
|
Some(true),
|
||||||
|
"Newest object version should be latest on the first listing"
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_original_version = first_versions
|
||||||
|
.iter()
|
||||||
|
.find(|version| version.version_id() == Some(first_version_id.as_str()))
|
||||||
|
.expect("First listing should include the original object version");
|
||||||
|
assert_eq!(
|
||||||
|
first_original_version.is_latest(),
|
||||||
|
Some(false),
|
||||||
|
"Original object version should no longer be latest"
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_delete_marker = first_delete_markers
|
||||||
|
.iter()
|
||||||
|
.find(|marker| marker.version_id() == Some(delete_marker_version_id.as_str()))
|
||||||
|
.expect("First listing should include the delete marker");
|
||||||
|
assert_eq!(
|
||||||
|
first_delete_marker.is_latest(),
|
||||||
|
Some(false),
|
||||||
|
"Delete marker should no longer be latest after the second put"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -891,7 +891,10 @@ impl LocalDisk {
|
|||||||
check_path_length(file_path.to_string_lossy().as_ref())?;
|
check_path_length(file_path.to_string_lossy().as_ref())?;
|
||||||
|
|
||||||
self.write_all_internal(&file_path, InternalBuf::Owned(buf), sync, skip_parent)
|
self.write_all_internal(&file_path, InternalBuf::Owned(buf), sync, skip_parent)
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
get_global_file_cache().invalidate(&file_path).await;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// write_all_internal do write file
|
// write_all_internal do write file
|
||||||
async fn write_all_internal(&self, file_path: &Path, data: InternalBuf<'_>, sync: bool, skip_parent: &Path) -> Result<()> {
|
async fn write_all_internal(&self, file_path: &Path, data: InternalBuf<'_>, sync: bool, skip_parent: &Path) -> Result<()> {
|
||||||
@@ -2122,6 +2125,9 @@ impl DiskAPI for LocalDisk {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_global_file_cache().invalidate(&src_file_path).await;
|
||||||
|
get_global_file_cache().invalidate(&dst_file_path).await;
|
||||||
|
|
||||||
if let Some(src_file_path_parent) = src_file_path.parent() {
|
if let Some(src_file_path_parent) = src_file_path.parent() {
|
||||||
if src_volume != super::RUSTFS_META_MULTIPART_BUCKET {
|
if src_volume != super::RUSTFS_META_MULTIPART_BUCKET {
|
||||||
let _ = remove_std(src_file_path_parent);
|
let _ = remove_std(src_file_path_parent);
|
||||||
|
|||||||
Reference in New Issue
Block a user