Files
seaweedfs/weed/s3api/s3api_object_routed_read.go
T
Chris Lu a6d72bc272 s3api: delete orphaned chunks only when the entry is confirmed absent (#11389)
* s3api: test for chunks deleted under an entry the filer committed

Issue #11387: the filer can report a create failure after inserting the
entry (e.g. a parent-directory creation failing post-insert). The error
arrives in the response rather than as a transport status, so it maps
to a definitive error and putToFiler deletes the chunks of the live
entry.

* s3api: confirmCreateLanded also reports a confirmed-absent entry

The verification a failed create runs can answer both directions: the
entry matching the uploaded chunks proves the write landed, and an
authoritative not-found proves the uploaded chunks are orphaned. Return
both outcomes so the cleanup path can gate on the fact rather than the
error class. An empty upload can never prove a landing, so a zero-chunk
entry match no longer upgrades the outcome.

* s3api: delete orphaned chunks only when the entry is confirmed absent

A failed create no longer skips verification based on the error class: the filer can fail after inserting the entry (issue #11387) and a partially-applied routed transaction can leave it behind too, both surfacing as definitive errors. Every failed create now resolves the entry's fate, and the uploaded chunks are deleted only when the entry is confirmed absent; anything unverifiable keeps them for vacuum.

* s3api: confirm absence on every filer the create could have committed on

A lock-path create fails over across filers, so the entry can live on a replica the routed owner has not caught up to; one not-found does not prove absence. The confirmation now queries the owner, the prior owner, and the failover set, declaring absent only when none of them has the entry.

* s3api: bound the reconciliation lookups confirmCreateLanded runs

The lookups ran on context.Background() under the object write lock, so a connected filer that never replies could stall the write path. One timeout now covers the whole enumeration; an expired budget fails the remaining lookups as uncertain, which keeps the chunks.
2026-09-18 12:30:07 -07:00

109 lines
3.9 KiB
Go

package s3api
import (
"context"
"errors"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// unreachableOwnerTTL is how long a route-by-key owner that just failed a read is
// skipped before being retried — long enough to spare a dead owner per-request
// dials, short enough to resume owner-first reads soon after it (or the ring) recovers.
const unreachableOwnerTTL = 2 * time.Second
// getObjectEntryRoutedByKey resolves an object's entry preferring the key's write
// owner (the same route key the write path hashes), so a read sees a just-written
// object without waiting for cross-filer replication. On the owner's ErrNotFound it
// probes the key's prior owner once during a rebalance window; falls back to
// getEntry when no owner is resolvable.
func (s3a *S3ApiServer) getObjectEntryRoutedByKey(bucket, object string) (*filer_pb.Entry, error) {
fullPath := util.NewFullPath(s3a.bucketDir(bucket), object)
owner := s3a.routableWriteOwner(bucket, object)
if owner == "" || s3a.filerClient == nil {
entry, _, _, err := filer_pb.GetEntry(context.Background(), s3a, fullPath)
return entry, err
}
// Skip an owner whose recent read hit a transport error; read local-first until
// it (or the ring) recovers, rather than re-dialing a dead owner every request.
preferred := owner
if s3a.ownerRecentlyUnreachable(owner) {
preferred = ""
}
dir, name := fullPath.DirAndName()
var entry *filer_pb.Entry
err := s3a.withFilerClientFailover(context.Background(), preferred, false, func(client filer_pb.SeaweedFilerClient) error {
resp, lookupErr := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
})
if lookupErr != nil {
return lookupErr
}
entry = resp.Entry
return nil
})
// A just-moved key may not have replicated to the new owner yet; consult its
// prior owner once while the ring change is within the cooling-off window.
if errors.Is(err, filer_pb.ErrNotFound) {
if prior := s3a.priorWriteOwner(bucket, object); prior != "" && prior != owner {
if priorEntry, priorErr := s3a.lookupEntryOnFiler(context.Background(), prior, dir, name); priorErr == nil {
return priorEntry, nil
}
}
}
return entry, err
}
func (s3a *S3ApiServer) priorWriteOwner(bucket, object string) pb.ServerAddress {
if object == "" || s3a.objectWriteLockClient == nil {
return ""
}
return s3a.objectWriteLockClient.PriorOwnerForKey(s3a.objectRouteKey(bucket, object))
}
func (s3a *S3ApiServer) markOwnerUnreachable(owner pb.ServerAddress) {
s3a.unreachableOwners.Store(owner, time.Now().Add(unreachableOwnerTTL))
}
func (s3a *S3ApiServer) ownerRecentlyUnreachable(owner pb.ServerAddress) bool {
if v, ok := s3a.unreachableOwners.Load(owner); ok {
return time.Now().Before(v.(time.Time))
}
return false
}
// lookupEntryPreferringOwner reads an entry back from a known write owner, so a
// caller that just wrote there sees its own write. Unlike getObjectEntryRoutedByKey
// it never drops the owner for a healthier peer, which would read behind the write.
func (s3a *S3ApiServer) lookupEntryPreferringOwner(owner pb.ServerAddress, dir, name string) (*filer_pb.Entry, error) {
if owner == "" {
return s3a.getEntry(dir, name)
}
return s3a.lookupEntryOnFiler(context.Background(), owner, dir, name)
}
// lookupEntryOnFiler resolves dir/name against a single filer, without failover.
func (s3a *S3ApiServer) lookupEntryOnFiler(ctx context.Context, filer pb.ServerAddress, dir, name string) (*filer_pb.Entry, error) {
var entry *filer_pb.Entry
err := pb.WithFilerClient(false, 0, filer, s3a.option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
resp, lookupErr := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
})
if lookupErr != nil {
return lookupErr
}
entry = resp.Entry
return nil
})
return entry, err
}