Files
seaweedfs/weed/s3api/s3api_object_versioned_finalize.go
T
Chris Lu 2d41f4ca8b s3: route non-conditional suspended DELETE off the DLM
A suspended DELETE removes the null version (the main object entry) and adds a
delete marker. Now that suspended PUT routes on the object key, a DELETE left on
the distributed lock wouldn't serialize against it (the per-path entry lock and
the distributed lock are independent). Route it on the object key too.

FinalizeVersionedWrite gains an optional delete_path it removes under the same
lock before flipping the pointer, so "delete null + add marker" is one atomic
step on the owner. createDeleteMarker threads it through (empty for the
versioning-enabled marker path). The DeleteObjectHandler routes a no-versionId
suspended delete when there is no If-Match — that condition targets the main
object, not the .versions pointer the op evaluates, so a conditional suspended
delete stays on the lock path. Object-lock can't apply (it requires versioning
enabled, never suspended).
2026-05-23 10:17:45 -07:00

163 lines
7.3 KiB
Go

package s3api
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// errVersionedPreconditionFailed is returned by versioned finalize helpers whose
// contract is (…, error) when the routed FinalizeVersionedWrite reports a failed
// precondition, so callers can surface it as 412 rather than a generic 500.
var errVersionedPreconditionFailed = errors.New("versioned write precondition failed")
// finalizeCodeToError adapts a routedVersionedFinalize result to an error: nil on
// success, the precondition sentinel on 412, else a generic error.
func finalizeCodeToError(code s3err.ErrorCode) error {
switch code {
case s3err.ErrNone:
return nil
case s3err.ErrPreconditionFailed:
return errVersionedPreconditionFailed
default:
return fmt.Errorf("versioned finalize failed: code %v", code)
}
}
// objectWriteOwner returns the filer that owns all of an object's writes, or ""
// when no ring view is available. It hashes the object key with the same prefix
// the non-versioned path uses (routedObjectOwner), so normal, suspended, and
// versioned writes to the same object all resolve to one owner and serialize on
// the same lock — regardless of versioning state. The whole object subtree
// (object, .versions dir, version files) belongs to this owner.
func (s3a *S3ApiServer) objectWriteOwner(bucket, object string) pb.ServerAddress {
if s3a.objectWriteLockClient == nil {
return ""
}
// Object-lock (WORM) buckets keep ALL of their writes on the distributed
// lock. Retention enforcement is a gateway-side check that the per-path-locked
// filer ops don't perform, so routing only some of such a bucket's writes
// would split serialization between the entry lock and the distributed lock
// (they don't serialize against each other). Excluding the bucket entirely
// keeps it internally consistent.
if locked, err := s3a.isObjectLockEnabled(bucket); err != nil || locked {
return ""
}
return s3a.objectWriteLockClient.PrimaryForKey("s3.object.write:" + s3a.toFilerPath(bucket, object))
}
// versionedPointerExtended computes the Extended keys to set and to clear on the
// .versions directory entry for a new latest version, mirroring
// setCachedListMetadata (clear cached fields, then set id/filename + cached
// size/mtime/etag/owner/delete-marker). The filer applies deletes before sets.
func versionedPointerExtended(versionId, versionFileName string, versionEntry *filer_pb.Entry) (set map[string][]byte, del []string) {
set = map[string][]byte{
s3_constants.ExtLatestVersionIdKey: []byte(versionId),
s3_constants.ExtLatestVersionFileNameKey: []byte(versionFileName),
}
if versionEntry.Attributes != nil {
set[s3_constants.ExtLatestVersionSizeKey] = []byte(strconv.FormatUint(versionEntry.Attributes.FileSize, 10))
set[s3_constants.ExtLatestVersionMtimeKey] = []byte(strconv.FormatInt(versionEntry.Attributes.Mtime, 10))
}
isDeleteMarker := []byte("false")
if versionEntry.Extended != nil {
if v, ok := versionEntry.Extended[s3_constants.ExtETagKey]; ok {
set[s3_constants.ExtLatestVersionETagKey] = v
}
if v, ok := versionEntry.Extended[s3_constants.ExtAmzOwnerKey]; ok {
set[s3_constants.ExtLatestVersionOwnerKey] = v
}
if v, ok := versionEntry.Extended[s3_constants.ExtDeleteMarkerKey]; ok {
isDeleteMarker = v
}
}
set[s3_constants.ExtLatestVersionIsDeleteMarker] = isDeleteMarker
del = []string{
s3_constants.ExtLatestVersionSizeKey,
s3_constants.ExtLatestVersionMtimeKey,
s3_constants.ExtLatestVersionETagKey,
s3_constants.ExtLatestVersionOwnerKey,
s3_constants.ExtLatestVersionIsDeleteMarker,
}
return set, del
}
// routedVersionedFinalize finalizes a versioned write by routing the precondition
// check, the demote of the previous latest, and the pointer flip to the owner of
// the object's .versions directory, where they run atomically under one local
// lock. The caller (putToFiler with uniqueVersionPath) has already created the
// version file and skips the object write lock and gateway precondition. A
// transport/in-band error maps to InternalError (the client retries); there is
// no silent non-atomic fallback.
func (s3a *S3ApiServer) routedVersionedFinalize(owner pb.ServerAddress, bucket, object, versionId, versionFileName string, versionEntry *filer_pb.Entry, cond *filer_pb.WriteCondition, deletePath string) s3err.ErrorCode {
set, del := versionedPointerExtended(versionId, versionFileName, versionEntry)
req := &filer_pb.FinalizeVersionedWriteRequest{
LockKey: s3a.toFilerPath(bucket, object),
DeletePath: deletePath,
VersionsDir: s3a.toFilerPath(bucket, object+s3_constants.VersionsFolder),
SetExtended: set,
DeleteExtended: del,
PriorLatestKey: s3_constants.ExtLatestVersionFileNameKey,
NoncurrentSinceKey: s3_constants.ExtNoncurrentSinceNsKey,
NoncurrentSinceNs: time.Now().UnixNano(),
Condition: cond,
LatestEtagKey: s3_constants.ExtLatestVersionETagKey,
LatestDeleteMarkerKey: s3_constants.ExtLatestVersionIsDeleteMarker,
}
var resp *filer_pb.FinalizeVersionedWriteResponse
err := pb.WithFilerClient(false, 0, owner, s3a.option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
var e error
resp, e = client.FinalizeVersionedWrite(context.Background(), req)
return e
})
switch {
case err != nil:
glog.Errorf("routedVersionedFinalize: %s/%s on %s: %v", bucket, object, owner, err)
return s3err.ErrInternalError
case resp.ErrorCode == filer_pb.FilerError_PRECONDITION_FAILED:
return s3err.ErrPreconditionFailed
case resp.ErrorCode != filer_pb.FilerError_OK:
if code, ok := filerErrorCodeToS3Error(resp.ErrorCode); ok {
return code
}
glog.Errorf("routedVersionedFinalize: %s/%s unexpected code %v", bucket, object, resp.ErrorCode)
return s3err.ErrInternalError
case resp.Error != "":
glog.Errorf("routedVersionedFinalize: %s/%s: %s", bucket, object, resp.Error)
return s3err.ErrInternalError
default:
return s3err.ErrNone
}
}
// versionedAfterCreate returns the putToFiler afterCreate hook for a versioned
// write: the routed atomic finalize when the .versions owner is known and the
// precondition reduces to a single primitive, otherwise the existing lock-based
// updateLatestVersionInDirectory path. The bool reports whether the routed path
// was selected, which the caller passes as putToFiler's uniqueVersionPath.
func (s3a *S3ApiServer) versionedAfterCreate(r *http.Request, bucket, object, versionId, versionFileName string) (func(*filer_pb.Entry) s3err.ErrorCode, bool) {
owner := s3a.objectWriteOwner(bucket, object)
cond, condOk := buildWriteCondition(r)
if owner != "" && condOk {
return func(versionEntry *filer_pb.Entry) s3err.ErrorCode {
return s3a.routedVersionedFinalize(owner, bucket, object, versionId, versionFileName, versionEntry, cond, "")
}, true
}
return func(versionEntry *filer_pb.Entry) s3err.ErrorCode {
if err := s3a.updateLatestVersionInDirectory(bucket, object, versionId, versionFileName, versionEntry); err != nil {
glog.Errorf("putVersionedObject: failed to update latest version in directory: %v", err)
return s3err.ErrInternalError
}
return s3err.ErrNone
}, false
}