From ec4458f84671a464181ff0dc0ab524a8b761b489 Mon Sep 17 00:00:00 2001 From: LeonWang0735 Date: Mon, 2 Feb 2026 12:43:07 +0800 Subject: [PATCH] Fix/correctly handle terraform s3 backend with versioned bucket (#1686) Co-authored-by: loverustfs --- .../src/version_id_regression_test.rs | 42 +++++++++++++++++++ crates/ecstore/src/set_disk.rs | 4 ++ rustfs/src/profiling/allocator.rs | 14 +++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/e2e_test/src/version_id_regression_test.rs b/crates/e2e_test/src/version_id_regression_test.rs index 51dcd961f..44d9acc90 100644 --- a/crates/e2e_test/src/version_id_regression_test.rs +++ b/crates/e2e_test/src/version_id_regression_test.rs @@ -395,4 +395,46 @@ mod tests { info!("✅ PASSED: Veeam backup workflow simulation completed successfully"); } + + #[tokio::test] + #[serial] + async fn test_terraform_put_after_delete() -> Result<(), Box> { + init_logging(); + + let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment"); + env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS"); + + // Use a versioned bucket for this test + let bucket = "terraform"; + + let client = env.create_s3_client(); + env.create_test_bucket(bucket).await?; + + let key = "terraform.tfstate"; + let response = client + .put_object() + .bucket(bucket) + .key(key) + .body(ByteStream::from(b"v1".to_vec())) + .send() + .await; + assert!(response.is_ok()); + + client.delete_object().bucket(bucket).key(key).send().await?; + + let response = client + .put_object() + .bucket(bucket) + .key(key) + .body(ByteStream::from(b"v1".to_vec())) + .send() + .await; + + assert!(response.is_ok()); + + let get_response = client.get_object().bucket(bucket).key(key).send().await; + assert!(get_response.is_ok(), "Object should exist after PUT"); + + Ok(()) + } } diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 43e2dc538..76e570ba8 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -3583,6 +3583,10 @@ impl SetDisks { match oi { Ok(oi) => { + // If top level is a delete marker proceed to upload. + if oi.delete_marker { + return None; + } if should_prevent_write(&oi, http_preconditions.if_none_match, http_preconditions.if_match) { return Some(StorageError::PreconditionFailed); } diff --git a/rustfs/src/profiling/allocator.rs b/rustfs/src/profiling/allocator.rs index 1973f6bcc..01d78cdd1 100644 --- a/rustfs/src/profiling/allocator.rs +++ b/rustfs/src/profiling/allocator.rs @@ -28,6 +28,10 @@ use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock, Weak}; +type AllocatorShardedMap = ShardedHashMap>)>; +type LazyAllocatorShardedMap = LazyLock; + +type AllocatorSampleHashMap = HashMap<*const Vec, (i64, i64, Arc>)>; /// A wrapper around a GlobalAlloc that samples allocations and records stack traces. pub struct TracingAllocator { inner: A, @@ -45,7 +49,7 @@ static ENABLED: AtomicBool = AtomicBool::new(false); // Global storage for profile data // Map: Address (usize) -> (Size (usize), StackTrace (Arc>)) // We store the Arc to keep the stack trace alive as long as the allocation is live. -static LIVE_ALLOCATIONS: LazyLock>)>> = LazyLock::new(|| ShardedHashMap::new(64)); +static LIVE_ALLOCATIONS: LazyAllocatorShardedMap = LazyLock::new(|| ShardedHashMap::new(64)); // Cache for deduplicating stack traces. // Map: StackHash (u64) -> Weak> @@ -216,7 +220,7 @@ fn dump_profile_inner(path: &Path) -> Result<(), String> { // Collect samples // Aggregate by Stack Trace Pointer (deduplication via Arc pointer) // Map: Arc pointer -> (Count, Bytes, Arc>) - let mut aggregated_samples: HashMap<*const Vec, (i64, i64, Arc>)> = HashMap::new(); + let mut aggregated_samples: AllocatorSampleHashMap = HashMap::new(); // Step 1: Collect data from LIVE_ALLOCATIONS while holding the lock (implicitly via iter) // We do NOT perform symbol resolution here to avoid deadlocks. @@ -233,8 +237,10 @@ fn dump_profile_inner(path: &Path) -> Result<(), String> { // Step 2: Process samples and resolve symbols (outside of LIVE_ALLOCATIONS lock) for (_key, (count, bytes, frames)) in aggregated_samples { - let mut sample = pb::Sample::default(); - sample.value = vec![count, bytes]; + let mut sample = pb::Sample { + value: vec![count, bytes], + ..Default::default() + }; // Process frames for &addr in frames.iter() {