mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 16:28:30 +01:00
s3: route single-entry object writes to the owner filer, off the DLM
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.
This commit is contained in:
1 parent
1e70bd3fc6
commit
21af729cb4
10 files changed
+512
-125
No files matched your search
@@ -0,0 +1,168 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// routedObjectOwner returns the filer that owns this object's metadata for
|
||||
// route-by-key, or ok=false when the object's writes must keep the distributed
|
||||
// lock. Versioned and object-lock buckets stay on the lock path: their
|
||||
// mutations span multiple entries / extra metadata checks a single conditional
|
||||
// create or delete does not cover. On any lookup error it falls back to be safe.
|
||||
func (s3a *S3ApiServer) routedObjectOwner(bucket, object string) (pb.ServerAddress, bool) {
|
||||
if object == "" || s3a.objectWriteLockClient == nil {
|
||||
return "", false
|
||||
}
|
||||
if configured, err := s3a.isVersioningConfigured(bucket); err != nil || configured {
|
||||
return "", false
|
||||
}
|
||||
if locked, err := s3a.isObjectLockEnabled(bucket); err != nil || locked {
|
||||
return "", false
|
||||
}
|
||||
lockKey := fmt.Sprintf("s3.object.write:%s", s3a.toFilerPath(bucket, object))
|
||||
owner := s3a.objectWriteLockClient.PrimaryForKey(lockKey)
|
||||
if owner == "" {
|
||||
return "", false
|
||||
}
|
||||
return owner, true
|
||||
}
|
||||
|
||||
// routedObjectWrite decides whether an object PUT can take the route-by-key
|
||||
// fast path: the metadata write goes straight to the key's owner filer, which
|
||||
// serializes it with its local per-path lock and evaluates the precondition,
|
||||
// instead of acquiring a distributed lock. It returns the owner filer and the
|
||||
// reduced precondition, or ok=false to fall back to withObjectWriteLock.
|
||||
//
|
||||
// The fast path additionally requires no post-create hook and a precondition
|
||||
// that reduces to one primitive.
|
||||
func (s3a *S3ApiServer) routedObjectWrite(r *http.Request, bucket, object string, hasAfterCreate bool) (owner pb.ServerAddress, cond *filer_pb.WriteCondition, ok bool) {
|
||||
if hasAfterCreate {
|
||||
return "", nil, false
|
||||
}
|
||||
cond, condOk := buildWriteCondition(r)
|
||||
if !condOk {
|
||||
return "", nil, false
|
||||
}
|
||||
owner, ok = s3a.routedObjectOwner(bucket, object)
|
||||
if !ok {
|
||||
return "", nil, false
|
||||
}
|
||||
return owner, cond, true
|
||||
}
|
||||
|
||||
// buildWriteCondition reduces the request's conditional headers to a single
|
||||
// WriteCondition primitive the filer can evaluate. It only handles the
|
||||
// unambiguous single-condition cases; anything else (header combinations,
|
||||
// time-based conditions, ETag lists, weak ETags) returns ok=false so the caller
|
||||
// keeps the existing gateway-side evaluation under the distributed lock.
|
||||
func buildWriteCondition(r *http.Request) (*filer_pb.WriteCondition, bool) {
|
||||
headers, errCode := parseConditionalHeaders(r)
|
||||
if errCode != s3err.ErrNone {
|
||||
return nil, false
|
||||
}
|
||||
if !headers.isSet {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_NONE}, true
|
||||
}
|
||||
// Time-based conditions are rare on writes; let the lock path handle them.
|
||||
if !headers.ifModifiedSince.IsZero() || !headers.ifUnmodifiedSince.IsZero() {
|
||||
return nil, false
|
||||
}
|
||||
hasMatch := headers.ifMatch != ""
|
||||
hasNoneMatch := headers.ifNoneMatch != ""
|
||||
switch {
|
||||
case hasMatch && !hasNoneMatch:
|
||||
if headers.ifMatch == "*" {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_EXISTS}, true
|
||||
}
|
||||
if etag, single := singleStrongETag(headers.ifMatch); single {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_ETAG_MATCH, Etag: etag}, true
|
||||
}
|
||||
return nil, false
|
||||
case hasNoneMatch && !hasMatch:
|
||||
if headers.ifNoneMatch == "*" {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_NOT_EXISTS}, true
|
||||
}
|
||||
if etag, single := singleStrongETag(headers.ifNoneMatch); single {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_ETAG_NOT_MATCH, Etag: etag}, true
|
||||
}
|
||||
return nil, false
|
||||
default:
|
||||
// Both If-Match and If-None-Match present — leave to the lock path.
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// singleStrongETag returns the normalized ETag when the header carries exactly
|
||||
// one strong ETag, and false for ETag lists or weak ("W/") ETags, which the
|
||||
// fast path does not replicate.
|
||||
func singleStrongETag(v string) (string, bool) {
|
||||
v = strings.TrimSpace(v)
|
||||
if strings.Contains(v, ",") {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(v, "W/") || strings.HasPrefix(v, "w/") {
|
||||
return "", false
|
||||
}
|
||||
return strings.Trim(v, `"`), true
|
||||
}
|
||||
|
||||
// createEntryOnFiler sends a CreateEntry directly to the given owner filer so
|
||||
// its local per-path lock serializes the write. The raw response is returned so
|
||||
// the caller can distinguish PRECONDITION_FAILED from other outcomes.
|
||||
func (s3a *S3ApiServer) createEntryOnFiler(owner pb.ServerAddress, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
var resp *filer_pb.CreateEntryResponse
|
||||
err := pb.WithFilerClient(false, 0, owner, s3a.option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
||||
var e error
|
||||
resp, e = client.CreateEntry(context.Background(), req)
|
||||
return e
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// buildDeleteCondition reduces a DeleteObject's If-Match header to a primitive.
|
||||
// DeleteObject only honors If-Match (matching checkDeleteIfMatch), so other
|
||||
// conditional headers are ignored here as they are on the existing path.
|
||||
func buildDeleteCondition(r *http.Request) (*filer_pb.WriteCondition, bool) {
|
||||
ifMatch := strings.TrimSpace(r.Header.Get(s3_constants.IfMatch))
|
||||
switch {
|
||||
case ifMatch == "":
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_NONE}, true
|
||||
case ifMatch == "*":
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_EXISTS}, true
|
||||
default:
|
||||
if etag, single := singleStrongETag(ifMatch); single {
|
||||
return &filer_pb.WriteCondition{Kind: filer_pb.WriteCondition_IF_ETAG_MATCH, Etag: etag}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// deleteEntryOnFiler sends a non-recursive DeleteEntry for the object directly
|
||||
// to its owner filer, which serializes it and evaluates the precondition under
|
||||
// the per-path lock. Flags mirror the unversioned delete path (doDeleteEntry).
|
||||
func (s3a *S3ApiServer) deleteEntryOnFiler(owner pb.ServerAddress, bucket, object string, cond *filer_pb.WriteCondition) (*filer_pb.DeleteEntryResponse, error) {
|
||||
dir, name := util.NewFullPath(s3a.bucketDir(bucket), object).DirAndName()
|
||||
var resp *filer_pb.DeleteEntryResponse
|
||||
err := pb.WithFilerClient(false, 0, owner, s3a.option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
||||
var e error
|
||||
resp, e = client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
IsDeleteData: true,
|
||||
IsRecursive: false,
|
||||
IgnoreRecursiveError: true,
|
||||
Condition: cond,
|
||||
})
|
||||
return e
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
Reference in new issue
Block a user