From 8982b4a3d22cfce112c08cd69effd05b025a352d Mon Sep 17 00:00:00 2001 From: Hauser Date: Mon, 14 Sep 2026 01:31:12 +0800 Subject: [PATCH] fix(tier): preserve cleanup scheduling for overwrites (#7766) Queue committed tier free-version cleanup receipts for PUT and materialized CopyObject overwrites of transitioned null versions, while keeping remote deletion behind the existing persisted free-version cleanup path. Tighten data-movement delete-marker retry equivalence by ignoring local bucket-incarnation fencing metadata, avoid retry fallback to the source pool, and keep version-list pagination from manufacturing an empty final page. Refresh the e2e-distributed selector hash for the current release test set. Co-authored-by: zhi22915 --- .config/e2e-distributed-selection.txt | 4 +- crates/ecstore/src/set_disk/ops/object.rs | 90 +++++++++++++++++++++-- crates/ecstore/src/store/list_objects.rs | 29 ++++++-- crates/ecstore/src/store/object.rs | 30 ++++++-- 4 files changed, 127 insertions(+), 26 deletions(-) diff --git a/.config/e2e-distributed-selection.txt b/.config/e2e-distributed-selection.txt index 7fcfabb4f..ae9da053b 100644 --- a/.config/e2e-distributed-selection.txt +++ b/.config/e2e-distributed-selection.txt @@ -1,2 +1,2 @@ -sha256-linux=563bff8f1171d6dbe166ff8440310dbe98430e466aa3ecd8dc39e3c872b320f7 -sha256-darwin=563bff8f1171d6dbe166ff8440310dbe98430e466aa3ecd8dc39e3c872b320f7 +sha256-linux=3b17d97e97dacff32414b8094d3086ed8a38d509ba88b0734b884166b30e3157 +sha256-darwin=3b17d97e97dacff32414b8094d3086ed8a38d509ba88b0734b884166b30e3157 diff --git a/crates/ecstore/src/set_disk/ops/object.rs b/crates/ecstore/src/set_disk/ops/object.rs index 328969c5d..434403737 100644 --- a/crates/ecstore/src/set_disk/ops/object.rs +++ b/crates/ecstore/src/set_disk/ops/object.rs @@ -383,7 +383,25 @@ fn record_committed_tier_free_version_receipt( free_version_id: Uuid, batch: bool, ) { - if let Some(sink) = opts.tier_free_version_receipt_sink.as_ref() + record_committed_tier_free_version_receipt_to_sink( + opts.tier_free_version_receipt_sink.as_ref(), + bucket, + object, + source, + free_version_id, + batch, + ); +} + +fn record_committed_tier_free_version_receipt_to_sink( + sink: Option<&crate::object_api::TierFreeVersionReceiptSink>, + bucket: &str, + object: &str, + source: &ObjectInfo, + free_version_id: Uuid, + batch: bool, +) { + if let Some(sink) = sink && let Err(err) = sink.record(source, free_version_id) { warn!( @@ -3822,13 +3840,18 @@ impl SetDisks { ); } fi.metadata = user_defined; - if fi.version_id.is_none_or(|id| id.is_nil()) && !opts.data_movement && expected_restore_operation_id.is_none() { - // Every disk must publish the same cleanup owner alongside a - // replaced null version. This transient key is not persisted - // on the new object; recovery discovers the free-version in - // the committed xl.meta even if this request is cancelled. - fi.set_tier_free_version_id(&Uuid::new_v4().to_string()); - } + let put_tier_free_version_id = + if fi.version_id.is_none_or(|id| id.is_nil()) && !opts.data_movement && expected_restore_operation_id.is_none() { + // Every disk must publish the same cleanup owner alongside a + // replaced null version. This transient key is not persisted + // on the new object; recovery discovers the free-version in + // the committed xl.meta even if this request is cancelled. + let free_version_id = Uuid::new_v4(); + fi.set_tier_free_version_id(&free_version_id.to_string()); + Some(free_version_id) + } else { + None + }; fi.mod_time = mod_time; fi.size = w_size as i64; fi.versioned = opts.versioned || opts.version_suspended; @@ -4304,6 +4327,9 @@ impl SetDisks { let commit_capacity_scope_token = opts.capacity_scope_token; let commit_replication_state = replication_state_to_filemeta(&opts.put_replication_state()); let commit_scanner_publication_lease_tokens = scanner_publication_lease_tokens; + let commit_put_tier_free_version_id = put_tier_free_version_id; + let commit_tier_free_version_receipt_sink = opts.tier_free_version_receipt_sink.clone(); + let commit_skip_free_version = opts.skip_free_version; let request_cancellation = operation_cancellation.clone(); tmp_cleanup_owned = true; @@ -4460,6 +4486,41 @@ impl SetDisks { return Err(err); } + let put_tier_free_version_source = + if commit_put_tier_free_version_id.is_some() && commit_tier_free_version_receipt_sink.is_some() { + match commit_set + .get_object_info( + &commit_bucket, + &commit_object, + &ObjectOptions { + no_lock: true, + metadata_cache_safe: false, + versioned: commit_versioned, + version_suspended: commit_version_suspended, + ..Default::default() + }, + ) + .await + { + Ok(source) => Some(source), + Err(err) if is_err_object_not_found(&err) || is_err_version_not_found(&err) => None, + Err(err) => { + debug!( + event = EVENT_LIFECYCLE_TRANSITIONED_DELETE_CLEANUP_OWNER, + component = LOG_COMPONENT_ECSTORE, + subsystem = LOG_SUBSYSTEM_SET_DISK, + bucket = %commit_bucket, + object = %commit_object, + error = ?err, + "Skipped opportunistic tier free-version receipt source capture" + ); + None + } + } + } else { + None + }; + Self::assign_rename_data_indexes(&mut parts_metadatas); let mut rename_result = SetDisks::rename_data_owned_with_fence( &commit_disks, @@ -4632,6 +4693,19 @@ impl SetDisks { let cleanup_disks = rename_commit.cleanup_disks; let old_current_size = rename_commit.old_current_size; let mut fi = rename_commit.committed_file_info; + if let (Some(source), Some(free_version_id)) = + (put_tier_free_version_source.as_ref(), commit_put_tier_free_version_id) + && transitioned_delete_publishes_free_version(source, &fi, commit_skip_free_version) + { + record_committed_tier_free_version_receipt_to_sink( + commit_tier_free_version_receipt_sink.as_ref(), + &commit_bucket, + &commit_object, + source, + free_version_id, + false, + ); + } if needs_immediate_heal { let mut request = rustfs_heal_contracts::heal_channel::create_heal_request_with_options( diff --git a/crates/ecstore/src/store/list_objects.rs b/crates/ecstore/src/store/list_objects.rs index 0b3683655..b1fa7cca1 100644 --- a/crates/ecstore/src/store/list_objects.rs +++ b/crates/ecstore/src/store/list_objects.rs @@ -2216,7 +2216,7 @@ fn list_objects_paginate( } } - if !is_truncated && disk_has_more { + if !is_truncated && disk_has_more && !include_version_id { let visible_count = objects.len() + prefixes.len(); let should_truncate = if delimiter.is_none() { visible_count > 0 @@ -7312,14 +7312,23 @@ mod test { fn test_object_meta_entry(name: &str) -> MetaCacheEntry { let mut meta = FileMeta::new(); - meta.add_version(FileInfo { - volume: "bucket".to_owned(), - name: name.to_owned(), + let mut metadata = HashMap::new(); + metadata.insert("etag".to_string(), "etag".to_string()); + let mut fi = FileInfo::new(name, 2, 2); + fi.erasure.index = 1; + fi.data_dir = Some(Uuid::from_u128(0x1234)); + fi.volume = "bucket".to_owned(); + fi.name = name.to_owned(); + fi.size = 1; + fi.parts = vec![ObjectPartInfo { + number: 1, size: 1, - mod_time: Some(time::OffsetDateTime::from_unix_timestamp(1_705_312_300).expect("valid timestamp")), + actual_size: 1, ..Default::default() - }) - .expect("test metadata should accept object version"); + }]; + fi.mod_time = Some(time::OffsetDateTime::from_unix_timestamp(1_705_312_300).expect("valid timestamp")); + fi.metadata = metadata; + meta.add_version(fi).expect("test metadata should accept object version"); let metadata = meta.marshal_msg().expect("test metadata should marshal"); MetaCacheEntry { @@ -7844,7 +7853,11 @@ mod test { } .expect("version page should list successfully"); let page_size = usize::try_from(max_keys).expect("nonnegative page size"); - assert_eq!(result.objects.len() + result.prefixes.len(), (10 - page * page_size).min(page_size)); + assert_eq!( + result.objects.len() + result.prefixes.len(), + (10 - page * page_size).min(page_size), + "{kind}, layer {layer}, max_keys {max_keys}, page {page}" + ); let has_more = page + 1 < expected_pages; assert_eq!(result.is_truncated, has_more, "{kind}, layer {layer}, max_keys {max_keys}, page {page}"); assert_eq!( diff --git a/crates/ecstore/src/store/object.rs b/crates/ecstore/src/store/object.rs index ccd2135fd..72f4f4bad 100644 --- a/crates/ecstore/src/store/object.rs +++ b/crates/ecstore/src/store/object.rs @@ -2056,7 +2056,7 @@ fn data_movement_delete_marker_metadata_identity(metadata: &HashMap