From c222317b84d92dab054d98a04ff9ecf0c93b7580 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 22 Aug 2026 15:39:04 +0800 Subject: [PATCH] fix(ecstore): complete latest identity checks --- crates/ecstore/src/store/rebalance.rs | 43 +++++++++++++++++++ crates/ecstore/src/store/rebalance/support.rs | 31 +++++++++---- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/crates/ecstore/src/store/rebalance.rs b/crates/ecstore/src/store/rebalance.rs index 4ad030a0c..68f64824b 100644 --- a/crates/ecstore/src/store/rebalance.rs +++ b/crates/ecstore/src/store/rebalance.rs @@ -1619,6 +1619,49 @@ mod tests { ); } + #[test] + fn resolve_latest_object_info_candidates_rejects_user_defined_identity_conflict() { + let base = object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string())); + let mut divergent = base.clone(); + divergent.user_defined = std::sync::Arc::new(std::collections::HashMap::from([( + "x-rustfs-internal-compression".to_string(), + "zstd".to_string(), + )])); + + assert_equal_time_identity_conflict(base, divergent); + } + + #[test] + fn resolve_latest_object_info_candidates_rejects_replication_identity_conflict() { + let base = object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string())); + + let mut replication = base.clone(); + replication.replication_status_internal = Some("PENDING".to_string()); + replication.replication_status = rustfs_filemeta::ReplicationStatusType::Pending; + assert_equal_time_identity_conflict(base.clone(), replication); + + let mut purge = base.clone(); + purge.version_purge_status_internal = Some("PENDING".to_string()); + purge.version_purge_status = rustfs_filemeta::VersionPurgeStatusType::Pending; + assert_equal_time_identity_conflict(base.clone(), purge); + + let mut decision = base; + decision.replication_decision = "replicate".to_string(); + assert_equal_time_identity_conflict( + object_info_with_identity(10, false, Uuid::from_u128(1), Some("etag-a".to_string())), + decision, + ); + } + + #[test] + fn resolve_latest_object_info_candidates_rejects_none_vs_unix_epoch_mod_time() { + let mut without_mod_time = object_info_with_identity(0, false, Uuid::from_u128(1), Some("etag-a".to_string())); + without_mod_time.mod_time = None; + let with_unix_epoch = object_info_with_identity(0, false, Uuid::from_u128(1), Some("etag-a".to_string())); + + assert_equal_time_identity_conflict(without_mod_time, with_unix_epoch); + } + #[test] fn resolve_latest_object_info_candidates_ignores_older_identity_conflicts() { let latest = object_info_with_identity(20, false, Uuid::from_u128(1), Some("etag-latest".to_string())); diff --git a/crates/ecstore/src/store/rebalance/support.rs b/crates/ecstore/src/store/rebalance/support.rs index 2efc0bffd..49c9ba1e1 100644 --- a/crates/ecstore/src/store/rebalance/support.rs +++ b/crates/ecstore/src/store/rebalance/support.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cmp::Ordering; - use crate::error::{Error, Result, StorageError, is_err_object_not_found, is_err_version_not_found}; use crate::object_api::{ObjectInfo, ObjectOptions}; use rustfs_utils::path::decode_dir_object; @@ -153,30 +151,47 @@ fn same_transition_identity(left: &ObjectInfo, right: &ObjectInfo) -> bool { && left.transitioned_object.status == right.transitioned_object.status } -/// Pool-specific erasure geometry is intentionally excluded. All fields that -/// identify the selected object version and its payload must agree before the -/// pool index can provide a deterministic tie-break. +/// Pool-specific erasure geometry is intentionally excluded: `get_object_info` +/// returns each pool's own `data_blocks`/`parity_blocks`, so those values can +/// differ for the same object version while the selected winner still carries +/// the chosen pool's layout. `put_object_reader` is also intentionally +/// excluded because it is a transient request handle that `ObjectInfo::clone` +/// drops. Every other ObjectInfo field is part of the production-visible +/// identity and must agree before the pool index can provide a deterministic +/// tie-break. fn same_latest_object_info_identity(left: &ObjectInfo, right: &ObjectInfo) -> bool { left.bucket == right.bucket && left.name == right.name && left.storage_class == right.storage_class + && left.mod_time == right.mod_time && left.size == right.size && left.actual_size == right.actual_size && left.is_dir == right.is_dir + && left.user_defined == right.user_defined + && left.user_tags == right.user_tags && left.version_id == right.version_id && left.data_dir == right.data_dir && left.delete_marker == right.delete_marker + && same_transition_identity(left, right) && left.restore_ongoing == right.restore_ongoing && left.restore_expires == right.restore_expires - && left.user_tags == right.user_tags + && left.parts == right.parts + && left.is_latest == right.is_latest && left.content_type == right.content_type && left.content_encoding == right.content_encoding && left.expires == right.expires + && left.num_versions == right.num_versions + && left.successor_mod_time == right.successor_mod_time && left.etag == right.etag && left.inlined == right.inlined - && left.parts == right.parts + && left.metadata_only == right.metadata_only + && left.version_only == right.version_only + && left.replication_status_internal == right.replication_status_internal + && left.replication_status == right.replication_status + && left.version_purge_status_internal == right.version_purge_status_internal + && left.version_purge_status == right.version_purge_status + && left.replication_decision == right.replication_decision && left.checksum == right.checksum - && same_transition_identity(left, right) } pub(super) fn resolve_latest_object_info_candidates(