Skip to content

Conditional DELETE: One Condition, One Logical Object

Single-object conditional deletion (If-Match on DeleteObject) landed in PR #145, and PR #178 repaired multi-pool serialization and reconciliation. Both changes shipped in Server 20260916.

An earlier August proposal around PR #12 sketched a broader scope—including batch DeleteObjects rejection, an extra s3:GetObject authorization check, and a current-version-only rule—that was not adopted in the merged implementation. This note documents the shipped behavior in f99ed829b and why precondition evaluation must sit above individual storage pools.

Implemented Contract

Request Shipped Behavior
Single DeleteObject, non-empty If-Match: <ETag> Evaluates the client-visible ETag under the outer deletion lock; mismatch returns 412 PreconditionFailed before any deletion
If-Match: * Requires an existing, non-delete-marker representation
Explicit versionId Evaluates the addressed version, not an unrelated current version
No If-Match, or an empty header value Installs no conditional callback; ordinary deletion applies
Batch DeleteObjects with per-item <ETag> ObjectToDelete has no ETag field; the XML element supplies no deletion protection
Internal recursive x-minio-force-delete Prefix deletion returns before the object-condition path; do not use it as conditional deletion

These preconditions do not add an s3:GetObject authorization check. Ordinary deletion authorization applies—including s3:DeleteObjectVersion for an explicitly addressed version, Object Lock checks, and the separate trusted-replication path.

Why the Condition Belongs Above Individual Pools

In a multi-pool deployment, an object can have copies of different ages in more than one pool. A request precondition applies to the single logical object selected for the operation. Evaluating and mutating independently inside each pool can delete one copy and then return 412 PreconditionFailed from another, or delete the newest copy and uncover a stale one.

The multi-pool coordinator (erasureServerPools.DeleteObject) therefore acquires the shared namespace write lock, gathers the relevant pool states, evaluates the condition once, clears the callback for lower layers, and reconciles the selected deletion across pools. The same lock boundary covers concurrent object writes and metadata updates.

Counterexamples from the Early Design

Suppose Pool 1 holds an older copy with ETag A and Pool 2 holds the current copy with ETag B:

  • If-Match: A (partial delete on 412): Per-pool evaluation deletes copy A in Pool 1 and only then fails with 412 PreconditionFailed against B in Pool 2, mutating storage on a rejected request.
  • If-Match: B (stale copy exposure after 204): Per-pool evaluation deletes B in Pool 2 while failing or skipping Pool 1, leaving stale copy A visible as the current object.

Executing an HTTP precondition callback inside per-pool workers also risks concurrent writes to a single http.ResponseWriter. Consuming the request condition once at the coordinating layer eliminates both hazards.

Failure Boundaries

The reconciliation path reads pool state before evaluating the condition and surfaces read quorum failures instead of treating an unreachable pool as empty. Physical cleanup errors can still occur after a partial mutation: a failed request is not a distributed multi-pool rollback transaction. Retrying and verifying object state remain necessary after a storage failure; see Multi-Pool Object Consistency.

Selection and Evaluation

Evaluate Once

erasureServerPools.DeleteObject holds the outer namespace lock. Multi-pool deployments call deleteObjectReconciled; single-pool deployments read the selected representation and invoke CheckPrecondFn before taking the delete-marker shortcut. Lower storage layers never reinterpret the precondition per copy.

Wildcards and Delete Markers

The DELETE precondition helper treats If-Match: * as requiring an active object representation. A delete marker does not satisfy *. A missing key or missing addressed version follows the normal not-found path and is never coerced into an empty-ETag match.

Authorization

The handler authorizes deletion against the effective target version (s3:DeleteObject or s3:DeleteObjectVersion). It does not require s3:GetObject when a specific ETag is supplied in If-Match.

SSE-C ETags

Conditional deletion compares the established client-visible ETag projection stored in metadata; it does not read or decrypt the object payload. SSE-C read-key authentication remains separate from object deletion.

Explicit Versions

Supplying an explicit versionId selects that exact version for both ETag comparison and deletion. A matching historical ETag allows deleting that historical version even when the current version has a different ETag.

Fail-Closed and Unsupported Edges

An empty If-Match header installs no precondition. The internal recursive prefix-delete extension (x-minio-force-delete) bypasses object preconditions. Batch XML <ETag> elements in DeleteObjects are not parsed by ObjectToDelete, and there is no request-wide NotImplemented rejection guard. Callers requiring compare-and-delete semantics must use single-object DeleteObject with a non-empty If-Match header.

Rejected Alternatives and Scope

  • Modifying a shared ETag comparator: Changing low-level string comparison cannot fix multi-pool winner selection or prevent mutations before a precondition check.
  • Aggregating errors after per-pool callbacks: Returning 412 after one pool has already deleted its copy cannot undo the physical deletion.
  • Introducing a new distributed transaction framework: Consuming one precondition callback under the existing outer namespace lock is sufficient for single-object DeleteObject; batch execution and policy keys require separate surface changes.

Source Evidence

The implementation lives in cmd/object-handlers.go, cmd/erasure-server-pool.go, and cmd/api-datatypes.go, with regression tests covering ETag mismatches, wildcard and delete-marker handling, explicit versionId targeting, quorum failures, and multi-pool reconciliation.

Follow-Up Work

Batch Conditions

Supporting per-item <ETag> preconditions in DeleteObjects requires unmarshaling the XML field, evaluating each logical object under its outer lock, honoring Quiet mode, and reporting per-item precondition failures in the XML result. Until that path is implemented, batch <ETag> elements are ignored and must not be used as a concurrency guard.

Policy Enforcement

The IAM policy package does not yet define the s3:if-match condition key. Supporting policy-enforced conditional deletes requires adding the condition key in silo-pkg, populating request condition values in the server handler, and adding policy evaluation tests.

Design Trade-Off

Single-object conditional DELETE reuses the existing multi-pool object-selection and locking boundary without inventing a new transaction layer. Full batch preconditions and IAM policy condition keys cross additional API and package boundaries and remain separate work. The core invariant is strict: a false single-object If-Match precondition is decided once against the selected logical representation before any pool deletes data.