objectWriteOwner now returns "" for object-lock (WORM) buckets, so versioned
PutObject / copy / delete-marker no longer route them off the lock. Routed writes
serialize on the owner's per-path entry lock while retention-checked deletes use
the distributed lock, and those two locks don't serialize against each other; an
object-lock bucket that routed some writes and not others would split-brain on
the same object. Retention enforcement is gateway-side and not part of the
per-path-locked filer ops, so the whole bucket stays on the distributed lock and
remains internally consistent. The now-redundant gate is dropped from
routedObjectOwner.
A suspended-versioning PUT writes the "null" version to the main object path, so
it is a single-entry object write just like a non-versioned PUT — only the
IsLatest-flag rewrite over existing versions differs, and that is best-effort
bookkeeping (recoverable on the next list-versions resync).
So route it on the object key like a normal PUT: putSuspendedVersioningObject
now calls putToFiler without an afterCreate hook (letting the route-by-key path
take it and skip the distributed lock), then runs updateIsLatestFlagsFor-
SuspendedVersioning best-effort after the write instead of inside the lock.
routedObjectOwner now excludes only versioning-*enabled* buckets (whose writes go
to .versions) rather than all versioning-configured buckets, so suspended and
unversioned writes — both targeting the main object path — resolve the same
object-key owner and serialize on the same lock. Object-lock buckets still keep
the lock path; suspended DELETE (delete null + create marker) stays multi-step on
the lock.
A non-versioned multipart completion's only contended mutation is the final
object CreateEntry; assembling parts and the idempotent-replay check are
read-only. So when the destination is non-versioned, route that final write to
the object's owner with the precondition (reusing the route-by-key path) and run
the assembly outside any lock, skipping the distributed lock and gateway
precondition.
mkFileRouted builds the object entry the way mkFile does, applies the existing
modifier, and routes the conditioned CreateEntry to the owner. Versioned and
suspended completions keep the lock path. This is a cold path (one completion per
upload), so the win is consistency with the other write paths rather than
throughput.
Address review feedback on the routed fast paths:
- PutObject: map the response's machine-readable FilerError code to the same S3
error the lock path produces (key-too-long, parent/existing-is-file,
existing-is-directory, precondition), instead of re-wrapping resp.Error in a
fresh error that filerErrorToS3Error can no longer match by sentinel. Unknown
codes and in-band errors fall back to the lock path for exact semantics.
- DeleteObject: on any non-precondition routed error, fall back to the lock
path instead of returning InternalError. This restores directory-marker
handling (demote a marker that still has children rather than failing on a
non-empty folder), which the raw DeleteEntry call lacks.
For non-versioned, non-object-lock buckets, PutObject and unversioned
DeleteObject now send their metadata write straight to the object key's owner
filer (resolved from the lock-ring view) with the precondition attached. The
owner serializes the write with its local per-path lock and evaluates the
condition atomically, so these paths no longer acquire a distributed lock.
The fast path is opt-in per request and falls back to withObjectWriteLock for
anything it does not fully cover: versioned or object-lock buckets, conditions
that do not reduce to a single primitive (ETag lists, weak ETags, time-based,
combined headers), an unresolved owner, or an unreachable owner filer. So
behavior is unchanged outside the safe subset.
Multi-step finalizations (copy, CompleteMultipartUpload) and versioned writes
keep the distributed lock: they mutate several entries under one held lock,
which a single conditional create or delete does not cover.
DeleteEntry gains the same optional WriteCondition and per-path lock as
CreateEntry so a routed conditional delete is atomic on the owner.