mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 00:26:53 +00:00
fix(replication): compute the PUT replication decision exactly once (#4934)
The PutObject usecase computed `must_replicate_object` twice with the same inputs: once before commit to persist the pending replication metadata, and again after commit to drive `schedule_object_replication`. Besides repeating the versioning/config/target traversal on the hot path, the two computations read the replication configuration independently, so a config hot update landing between them could split the two phases into a pending-without-schedule or schedule-without-pending divergence. Reuse the single immutable `ReplicateDecision` computed before commit for both the pending metadata and the post-commit schedule. The two former computations were already equivalent for a stable config (`must_replicate` reads only `opts.replication_request` from the options, which the pending-suffix insertion does not touch), so this preserves replication semantics while removing the redundant traversal and closing the config-race window. The decision carries only stable target/rule/status identifiers (arn, replicate, synchronous, id) and no secrets. Add a white-box regression test that drives a real PutObject through the usecase and asserts, via a test-only invocation counter on `must_replicate_object`, that a single PUT computes the decision exactly once; reverting to the pre-commit + post-commit recompute makes the counter observe 2 and fails the test. Refs: https://github.com/rustfs/backlog/issues/1320
This commit is contained in:
@@ -4054,6 +4054,11 @@ impl DefaultObjectUsecase {
|
||||
.map(|ctx| ctx.request_id.clone())
|
||||
.unwrap_or_else(|| request_context::RequestContext::fallback().request_id);
|
||||
|
||||
// Compute the replication decision exactly once per PUT. The same
|
||||
// immutable `dsc` drives both the pending metadata written below and the
|
||||
// post-commit schedule (see the reuse site further down), so a
|
||||
// replication-config hot update can no longer split the two phases
|
||||
// (https://github.com/rustfs/backlog/issues/1320).
|
||||
let dsc =
|
||||
must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts.clone())
|
||||
.await;
|
||||
@@ -4193,9 +4198,15 @@ impl DefaultObjectUsecase {
|
||||
|
||||
let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag));
|
||||
|
||||
let dsc = must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts).await;
|
||||
let expiration = resolve_put_object_expiration(&bucket, &obj_info).await;
|
||||
|
||||
// Reuse the single replication decision computed before commit (see `dsc`
|
||||
// above) so the pending metadata persisted with the object and the
|
||||
// post-commit schedule always derive from the same immutable decision.
|
||||
// Recomputing here would repeat the versioning/config/target traversal and,
|
||||
// worse, allow a replication-config hot update between the two phases to
|
||||
// produce a pending-without-schedule or schedule-without-pending divergence
|
||||
// (https://github.com/rustfs/backlog/issues/1320).
|
||||
if dsc.replicate_any() {
|
||||
schedule_object_replication(obj_info.clone(), store, dsc).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user