diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 1421af1f2..3dbc4c784 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -1330,18 +1330,22 @@ func (s3a *S3ApiServer) putSuspendedVersioningObject(r *http.Request, bucket, ob // it runs while withObjectWriteLock is still held in putToFiler. // Doing it after putToFiler returns would race a concurrent PUT // promoting a newer latest, which we'd then incorrectly wipe. - etag, errCode, sseMetadata = s3a.putToFiler(r, filePath, body, bucket, normalizedObject, 1, 0, func(_ *filer_pb.Entry) s3err.ErrorCode { - if err := s3a.updateIsLatestFlagsForSuspendedVersioning(bucket, normalizedObject); err != nil { - // Best-effort: a stale IsLatest flag is recoverable on the - // next list-versions resync, so don't fail the PUT. - glog.Warningf("putSuspendedVersioningObject: failed to update IsLatest flags: %v", err) - } - return s3err.ErrNone - }, false) + // The null version is written to the main object path, so this is a + // single-entry object write — route it on the object key like a normal PUT + // (no afterCreate, so putToFiler can take the route-by-key path and skip the + // distributed lock). The IsLatest flag rewrite over existing versions is + // best-effort bookkeeping, so it runs after the write rather than inside the + // atomic boundary. + etag, errCode, sseMetadata = s3a.putToFiler(r, filePath, body, bucket, normalizedObject, 1, 0, nil, false) if errCode != s3err.ErrNone { glog.Errorf("putSuspendedVersioningObject: failed to upload object: %v", errCode) return "", errCode, SSEResponseMetadata{} } + if err := s3a.updateIsLatestFlagsForSuspendedVersioning(bucket, normalizedObject); err != nil { + // Best-effort: a stale IsLatest flag is recoverable on the next + // list-versions resync, so don't fail the PUT. + glog.Warningf("putSuspendedVersioningObject: failed to update IsLatest flags: %v", err) + } glog.V(2).Infof("putSuspendedVersioningObject: successfully created null version for %s/%s", bucket, object) diff --git a/weed/s3api/s3api_object_routed_write.go b/weed/s3api/s3api_object_routed_write.go index 1273f7aab..6a91bbed3 100644 --- a/weed/s3api/s3api_object_routed_write.go +++ b/weed/s3api/s3api_object_routed_write.go @@ -2,7 +2,6 @@ package s3api import ( "context" - "fmt" "net/http" "strings" "time" @@ -16,22 +15,23 @@ import ( ) // 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. +// route-by-key on a single-entry object write, or ok=false when the write must +// keep the distributed lock. Only versioning-*enabled* buckets are excluded: +// their writes go to /.versions and flip the latest pointer (the versioned +// finalize path handles those). Suspended and unversioned writes both go to the +// main object path, so they route here. Object-lock buckets stay on the lock +// path. 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 { + if object == "" { return "", false } - if configured, err := s3a.isVersioningConfigured(bucket); err != nil || configured { + if enabled, err := s3a.isVersioningEnabled(bucket); err != nil || enabled { 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) + owner := s3a.objectWriteOwner(bucket, object) if owner == "" { return "", false }