mirror of
https://github.com/pgsty/minio.git
synced 2026-09-25 20:35:58 +03:00
fix: retransmit and re-order Object Lock for SSE-C replicas (single erasure set)
Issue #120 routes an existing SSE-C replica through PutObjectHandler and NewMultipartUploadHandler. On main those handlers assigned the incoming retention and legal hold directly, without the source-timestamp ordering #111 added to CopyObjectHandler and without persisting the ordering timestamps, so in active-active replication a retransmit carrying an older value could overwrite a destination version's newer lock state. Share #111's ordering decision as applyReplicatedObjectLock in cmd/bucket-object-lock.go and call it from CopyObject, PUT and multipart initiation. A request that is not an actual trusted replica keeps ordinary write semantics (a validated value is applied and stamped now); only a real replica update is ordered against the stored version, so a marker-only peer write no longer drops a validated hold or default retention. CopyObject keeps its SSE-C key-rotation encMetadata reconciliation inline. putReplicationOpts now emits a stored retention ordering timestamp even when the value keys are absent, so a removal recorded on the retransmit PUT path still replicates onward. replicateAll marks Failed and carries the error when putReplicationOpts fails. The handler decision is made against the version as it stands then, which a concurrent lock update can outrun before the write commits, and for multipart across the whole initiation-to-completion span. Close that window under the object write lock the receiving erasure set holds: a trusted SSE-C replica full write sets ObjectOptions.ReplicaLockReconcile, and erasureObjects.PutObject and CompleteMultipartUpload re-run the ordering (reconcileStoredObjectLock, which orders retention and legal hold independently by their reserved timestamps) against the destination version read on that set before committing. Persisted upload metadata records the null version as an empty VersionID, so completion looks that up as the null version rather than the latest. The reconcile runs only against an existing version; a not-found destination keeps the write's own accepted lock, including a pre-upgrade upload that persisted values without ordering timestamps, and a non-not-found read error fails the write. Scoped to the SSE-C paths this issue enables; CopyObject is left as #111 wrote it. Scope: this orders Object Lock against the destination version under the write lock and is correct for a single erasure set. A multi-pool deployment -- where a version can have duplicate copies across pools, object ModTime ties do not track per-field lock timestamps, and the object namespace lock is per-pool -- needs a cross-pool lock-safe reconcile and is deliberately out of scope here, tracked in pgsty/silo#TBD-multipool-lock. Tests: TestAPISSECReplicaRetransmitObjectLockOrdering and its multipart sibling; TestAPIReplicaMultipartNewerHoldSurvivesCompletion and TestReplicaPutObjectLockReconcileUnderWriteLock (a hold or retention reaching the version after the handler decision, or after multipart initiation, survives the commit; a pre-upgrade upload on an absent version keeps its lock); TestReplicaLockReconcileNullVersion (a null-version completion reconciles the null version, not a coexisting UUID version, and an absent null version keeps its accepted lock); TestAPIReplicaMarkerOnlyAppliesObjectLock; TestReplicaStoredLock; the timestamp-only putReplicationOpts round trip; and the retransmit, exemption and target-head tests. The #111 CopyObject replica suite and the existing #120 suite stay green, as do the PUT/multipart handler and object-layer regression suites. Compatibility: the shared helper preserves #111's CopyObject behavior; a non-replica PUT or multipart initiation that sets Object Lock now also stamps the reserved ordering timestamp, matching CopyObject since #111; only trusted SSE-C replica writes take the in-lock reconcile. Refs pgsty/silo#120 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -877,8 +877,8 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (put
|
||||
if hasMode {
|
||||
putOpts.Mode = minio.RetentionMode(mode)
|
||||
}
|
||||
// A removed retention is stored as an empty mode and date; it is sent as
|
||||
// a value-less update that still carries its ordering timestamp.
|
||||
// A removed retention is stored as an empty or absent mode and date; it is
|
||||
// sent as a value-less update that still carries its ordering timestamp.
|
||||
if hasRetainDate && retainDateStr != "" {
|
||||
rdate, err := amztime.ISO8601Parse(retainDateStr)
|
||||
if err != nil {
|
||||
@@ -886,10 +886,14 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (put
|
||||
}
|
||||
putOpts.RetainUntilDate = rdate
|
||||
}
|
||||
if hasMode || hasRetainDate {
|
||||
// set retention timestamp in opts
|
||||
retainTmstampStr, hasRetainTmstamp := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]
|
||||
if hasMode || hasRetainDate || hasRetainTmstamp {
|
||||
// Send the ordering timestamp whenever the version carries one, even for a
|
||||
// removal whose value keys are absent (the shape a retransmit PUT leaves),
|
||||
// so the next hop can order the removal instead of keeping obsolete
|
||||
// retention.
|
||||
retTimestamp := objInfo.ModTime
|
||||
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||
if hasRetainTmstamp {
|
||||
var err error
|
||||
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
|
||||
if err != nil {
|
||||
@@ -1618,8 +1622,11 @@ applyAction:
|
||||
} else {
|
||||
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||
if err != nil {
|
||||
rinfo.Err = err
|
||||
// rinfo was primed Completed above; a failure to build the write
|
||||
// options means nothing reached the target, so mark it Failed and
|
||||
// carry the error instead of reporting a phantom success.
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
rinfo.Err = err
|
||||
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
|
||||
Reference in New Issue
Block a user