From 32845e03b0a4d06bcd2e9afe23b0d004f7a12eb0 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Mon, 6 Jul 2026 21:10:50 +0800 Subject: [PATCH] fix(replication): mark version-id fallback ETag match as Completed, not Failed (backlog#860) (#4310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During resync against an AWS-style target that rejects RustFS UUID versionIds, the code retries HEAD without a versionId and compares ETags. On a match it set `replication_action = None` ("already in sync, nothing to copy") but did not return, so control fell through to `if replication_action != ReplicationAction::All` — a branch meant only for the unsupported metadata-only case — and stamped the object FAILED with "metadata-only replication is not implemented". The target already held an identical object, yet the source recorded FAILED forever, so AWS-style targets never converged and the MRF kept re-queuing. Handle `ReplicationAction::None` explicitly before that branch: record it as Completed (with the resync timestamp/`replication_resynced` bookkeeping for ExistingObject + reset_id, mirroring the HEAD-success None path) and return. Only `ReplicationAction::Metadata` now takes the metadata-unsupported failure branch; `All` still proceeds to the copy. This path is the only way `None` reaches that point (the HEAD-success None case already returns earlier). Refs backlog#799 (B11). --- .../replication/replication_resyncer.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/ecstore/src/bucket/replication/replication_resyncer.rs b/crates/ecstore/src/bucket/replication/replication_resyncer.rs index 195c46c7b..7e4bcfc41 100644 --- a/crates/ecstore/src/bucket/replication/replication_resyncer.rs +++ b/crates/ecstore/src/bucket/replication/replication_resyncer.rs @@ -2658,6 +2658,28 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo { rinfo.size = size; rinfo.replication_action = replication_action; + if replication_action == ReplicationAction::None { + // The target already holds a matching object (reached here only via + // the version-id fallback ETag match above) — there is nothing to + // copy. Record it as synced and return, instead of falling into the + // metadata-unsupported failure branch below, which previously left + // AWS-style targets permanently FAILED and never converging + // (backlog#860 / #799 B11). + if self.op_type == ReplicationType::ExistingObject && !tgt_client.reset_id.is_empty() { + rinfo.resync_timestamp = format!( + "{};{}", + OffsetDateTime::now_utc() + .format(&Rfc3339) + .unwrap_or_else(|_| "invalid-time".to_string()), + tgt_client.reset_id + ); + rinfo.replication_resynced = true; + } + rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs(); + return rinfo; + } + + // action == Metadata: metadata-only replication is not implemented. if replication_action != ReplicationAction::All { rinfo.replication_status = ReplicationStatusType::Failed; rinfo.error = Some(ERR_REPLICATION_METADATA_COPY_UNSUPPORTED.to_string());