From a42320848ca70f6d7150247b5d94345744a8fe02 Mon Sep 17 00:00:00 2001 From: weisd Date: Sun, 22 Mar 2026 11:30:29 +0800 Subject: [PATCH] fix(ecstore): invalidate xl.meta cache after writes and rename (#2255) --- crates/e2e_test/src/lib.rs | 5 + .../list_object_versions_regression_test.rs | 182 ++++++++++++++++++ crates/ecstore/src/disk/local.rs | 8 +- 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 crates/e2e_test/src/list_object_versions_regression_test.rs diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index f39fa7ae7..16d403eac 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -66,6 +66,11 @@ mod compression_test; // Regression test for Issue #1878: DeleteMarkers not visible immediately after delete_objects #[cfg(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)] mod protocols; diff --git a/crates/e2e_test/src/list_object_versions_regression_test.rs b/crates/e2e_test/src/list_object_versions_regression_test.rs new file mode 100644 index 000000000..f5189c4b0 --- /dev/null +++ b/crates/e2e_test/src/list_object_versions_regression_test.rs @@ -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" + ); + } +} diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index d5fcf72ff..ecb334a27 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -891,7 +891,10 @@ impl LocalDisk { check_path_length(file_path.to_string_lossy().as_ref())?; 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 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); } + 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 src_volume != super::RUSTFS_META_MULTIPART_BUCKET { let _ = remove_std(src_file_path_parent);