mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-25 09:32:45 +00:00
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).
163 lines
7.3 KiB
Go
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
|
|
}
|