Files
seaweedfs/weed/server/filer_grpc_server_dlm.go
T
b747e0d3b1 cluster: bound each DistributedLock attempt with a timeout (#12113)
* cluster: bound each DistributedLock attempt with a timeout

An unreachable lock owner blocked the attempt on the connection timeout.
Bound it so the write fails fast; a timed-out attempt keeps no renew token.

* cluster: keep renewToken across failed lock attempts

A request may have renewed the lease server-side before its response was
lost, so the last token can still be valid on the next attempt. Clearing
it on any transport failure strands a live lease until its TTL expires.
A definitive rejection still clears the token through the response's
empty RenewToken, and falling back to the seed filer no longer abandons
the lease either.

* cluster: bound each lock attempt by the remaining route window

The 15s unavailability window started only after the first failure, so
three 10s attempts plus sleeps outlasted it: the third failure selected
the seed fallback at the same moment the loop gave up, and a reachable
seed route was never tried.

Start the clock before the first attempt and cap each attempt at what
the window has left. When the window is spent on a non-seed target,
fall back to the seed filer — it can forward to the real owner — and
give that route its own window, so the fallback is always exercised
before acquisition fails.

* cluster: bound DistributedUnlock by the lock RPC timeout

The unlock used context.Background(), so Stop against an unreachable
lock server could wait on the TCP connect timeout far past the renewal
goroutine's bounded shutdown wait. A timed-out unlock is harmless: the
lease expires via its TTL regardless.

* dlm: replay the current token on a lost renewal's retry

A renewal rotates the token server-side before the response is sent. When
that response is lost the client only knows the superseded token: retries
mismatched, the token was cleared, and the holder conflicted with its own
lease until it expired. The lock now remembers the superseded token, so a
retry from the same owner replays the current token, and a superseded
token can still release its own lock.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* cluster: give the seed lock fallback one fresh window per acquisition

The seed can redirect back to a dead primary via LockHostMovedTo, so
refilling the retry window on every return to the seed made the
primary->seed->primary cycle unbounded. The first seed fallback keeps
its fresh window; later returns to the seed keep the running deadline.

* cluster: advance the lock seq when a lost-renewal replay extends the lease

A retry of a renewal whose response was lost updates ExpiredAtNs in place
but returned the existing Seq, so the replicated extension was rejected
by InsertBackupLock as a stale mutation. A promoted backup would then
grant the lock when the old expiry passed even though the holder still
believes the lease is renewed. Bump Seq on the replay so the backup
accepts the new expiry.

* cluster: carry the superseded lock token through replication and transfer

A renewal rotates the lock token and keeps the superseded one in
PrevToken so a holder whose renewal response was lost can still recover.
That recovery broke as soon as the lock moved: neither the replicated
backup copy nor a transferred lock carried PrevToken, so a new primary
rejected retries of the old token until the lease expired.

Add prev_renew_token to the Lock and ReplicateLockRequest messages and
thread it through ReplicateLock, TransferLocks, InsertBackupLock and
InsertLock.

* cluster: count clean lock redirects against the retry window

A bare LockHostMovedTo response (no error, no token) returns nil from
attemptToLockWithin, and retryUntilLocked reset the retry window on every
nil result. Two ring-disagreeing filers forwarding to each other therefore
bounced the client in an unbounded loop; the spent-window exit could also
return a nil error with no token, handing callers an unacquired lock.

* cluster: do not mark a lock held or hot-loop on tokenless attempts

A bare LockHostMovedTo redirect returns nil with no renewToken; the
attempt previously flipped isLocked to 1 anyway and retried immediately.
Gate the flag on a granted token and pace the retry like other outcomes.

* cluster: keep renewToken across bare lock redirects

A LockHostMovedTo response carries no token and no error; overwriting
renewToken with it dropped a held token, so the next renewal re-acquired
fresh and could contend against its own still-live lease until TTL.

* cluster: poll for replicated backup state in lock tests

Replication runs in a goroutine, so a fixed sleep followed by an
immediate backup read is racy. Wait for the expected backup state
with require.Eventually instead.

* cluster: mark a lock held only on a grant verdict

A bare LockHostMovedTo redirect with a saved token left isLocked set
without the redirected filer granting anything, and retryUntilLocked
could exit on a stale token with no attempt made. doLock now reports
whether the response was a grant, and both the retry loop and isLocked
follow that verdict.

---------

Co-authored-by: chrislusf <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 20:53:46 -07:00

283 lines
10 KiB
Go

package weed_server
import (
"context"
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// DistributedLock is a grpc handler to handle FilerServer's LockRequest
func (fs *FilerServer) DistributedLock(ctx context.Context, req *filer_pb.LockRequest) (resp *filer_pb.LockResponse, err error) {
glog.V(4).Infof("FILER LOCK: Received DistributedLock request - name=%s owner=%s renewToken=%s secondsToLock=%d isMoved=%v",
req.Name, req.Owner, req.RenewToken, req.SecondsToLock, req.IsMoved)
resp = &filer_pb.LockResponse{}
var movedTo pb.ServerAddress
expiredAtNs := time.Now().Add(time.Duration(req.SecondsToLock) * time.Second).UnixNano()
var generation int64
resp.LockOwner, resp.RenewToken, generation, movedTo, err = fs.filer.Dlm.LockWithTimeout(req.Name, expiredAtNs, req.RenewToken, req.Owner)
resp.Generation = generation
glog.V(4).Infof("FILER LOCK: LockWithTimeout result - name=%s lockOwner=%s renewToken=%s movedTo=%s err=%v",
req.Name, resp.LockOwner, resp.RenewToken, movedTo, err)
glog.V(4).Infof("lock %s %v %v %v, isMoved=%v %v", req.Name, req.SecondsToLock, req.RenewToken, req.Owner, req.IsMoved, movedTo)
if mErr := fs.checkMovedMarker(ctx, req.IsMoved, movedTo); mErr != nil {
err = mErr
} else if !req.IsMoved && movedTo != "" && movedTo != fs.option.Host {
glog.V(3).Infof("FILER LOCK: Forwarding to correct filer - from=%s to=%s", fs.option.Host, movedTo)
err = pb.WithFilerClient(false, 0, movedTo, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
secondResp, err := client.DistributedLock(ctx, &filer_pb.LockRequest{
Name: req.Name,
SecondsToLock: req.SecondsToLock,
RenewToken: req.RenewToken,
IsMoved: true,
Owner: req.Owner,
})
if err == nil {
resp.RenewToken = secondResp.RenewToken
resp.LockOwner = secondResp.LockOwner
resp.Error = secondResp.Error
resp.Generation = secondResp.Generation
if forwardedLockAcquired(secondResp) {
glog.V(3).Infof("FILER LOCK: Forwarded lock acquired - name=%s renewToken=%s", req.Name, resp.RenewToken)
}
} else {
glog.V(0).Infof("FILER LOCK: Forward failed - name=%s err=%v", req.Name, err)
}
return err
})
}
if err != nil {
resp.Error = fmt.Sprintf("%v", err)
if !strings.Contains(resp.Error, "lock already owned") {
glog.V(0).Infof("FILER LOCK: Error - name=%s error=%s", req.Name, resp.Error)
}
}
if movedTo != "" {
resp.LockHostMovedTo = string(movedTo)
}
glog.V(4).Infof("FILER LOCK: Returning response - name=%s renewToken=%s lockOwner=%s error=%s movedTo=%s",
req.Name, resp.RenewToken, resp.LockOwner, resp.Error, resp.LockHostMovedTo)
return resp, nil
}
func forwardedLockAcquired(resp *filer_pb.LockResponse) bool {
return resp != nil && resp.Error == "" && resp.RenewToken != ""
}
// Unlock is a grpc handler to handle FilerServer's UnlockRequest
func (fs *FilerServer) DistributedUnlock(ctx context.Context, req *filer_pb.UnlockRequest) (resp *filer_pb.UnlockResponse, err error) {
resp = &filer_pb.UnlockResponse{}
var movedTo pb.ServerAddress
movedTo, err = fs.filer.Dlm.Unlock(req.Name, req.RenewToken)
if mErr := fs.checkMovedMarker(ctx, req.IsMoved, movedTo); mErr != nil {
err = mErr
} else if !req.IsMoved && movedTo != "" {
err = pb.WithFilerClient(false, 0, movedTo, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
secondResp, err := client.DistributedUnlock(ctx, &filer_pb.UnlockRequest{
Name: req.Name,
RenewToken: req.RenewToken,
IsMoved: true,
})
if err != nil {
return err
}
resp.Error = secondResp.Error
return nil
})
}
if err != nil {
resp.Error = fmt.Sprintf("%v", err)
}
if movedTo != "" {
resp.MovedTo = string(movedTo)
}
return resp, nil
}
func (fs *FilerServer) FindLockOwner(ctx context.Context, req *filer_pb.FindLockOwnerRequest) (*filer_pb.FindLockOwnerResponse, error) {
owner, movedTo, err := fs.filer.Dlm.FindLockOwner(req.Name)
if mErr := fs.checkMovedMarker(ctx, req.IsMoved, movedTo); mErr != nil {
return nil, mErr
}
if !req.IsMoved && movedTo != "" {
err = pb.WithFilerClient(false, 0, movedTo, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
secondResp, err := client.FindLockOwner(ctx, &filer_pb.FindLockOwnerRequest{
Name: req.Name,
IsMoved: true,
})
if err != nil {
return err
}
owner = secondResp.Owner
return nil
})
if err != nil {
return nil, err
}
}
if owner == "" {
glog.V(0).Infof("find lock %s moved to %v: %v", req.Name, movedTo, err)
return nil, status.Error(codes.NotFound, fmt.Sprintf("lock %s not found", req.Name))
}
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &filer_pb.FindLockOwnerResponse{
Owner: owner,
}, nil
}
// TransferLocks is a grpc handler to handle FilerServer's TransferLocksRequest.
// Preserves generation/seq and re-replicates to this node's backup.
func (fs *FilerServer) TransferLocks(ctx context.Context, req *filer_pb.TransferLocksRequest) (*filer_pb.TransferLocksResponse, error) {
for _, lock := range req.Locks {
fs.filer.Dlm.InsertLock(lock.Name, lock.ExpiredAtNs, lock.RenewToken, lock.PrevRenewToken, lock.Owner, lock.Generation, lock.Seq)
}
return &filer_pb.TransferLocksResponse{}, nil
}
// ReplicateLock handles lock replication from a primary to this backup node.
// Uses seq for causal ordering — rejects stale mutations.
func (fs *FilerServer) ReplicateLock(ctx context.Context, req *filer_pb.ReplicateLockRequest) (*filer_pb.ReplicateLockResponse, error) {
if req.IsUnlock {
fs.filer.Dlm.RemoveBackupLockIfSeq(req.Name, req.Generation, req.Seq)
glog.V(4).Infof("FILER REPLICATE: removed backup lock %s generation=%d seq=%d", req.Name, req.Generation, req.Seq)
} else {
fs.filer.Dlm.InsertBackupLock(req.Name, req.ExpiredAtNs, req.RenewToken, req.PrevRenewToken, req.Owner, req.Generation, req.Seq)
glog.V(4).Infof("FILER REPLICATE: inserted backup lock %s owner=%s generation=%d seq=%d", req.Name, req.Owner, req.Generation, req.Seq)
}
return &filer_pb.ReplicateLockResponse{}, nil
}
// OnDlmChangeSnapshot is called when the lock ring topology changes.
// It handles:
// 1. Promoting backup locks to primary when this node becomes the new primary
// 2. Re-replicating to the new backup
// 3. Transferring locks that no longer belong here
func (fs *FilerServer) OnDlmChangeSnapshot(snapshot []pb.ServerAddress) {
locks := fs.filer.Dlm.AllLocks()
if len(locks) == 0 {
return
}
for _, lock := range locks {
primary, backup := fs.filer.Dlm.LockRing.GetPrimaryAndBackup(lock.Key)
if primary == fs.option.Host {
if lock.IsBackup {
// We held this as backup, now we're primary → promote
fs.filer.Dlm.PromoteLock(lock.Key)
glog.V(0).Infof("DLM: promoted backup lock %s to primary", lock.Key)
}
// Replicate to (possibly new) backup
if backup != "" && fs.filer.Dlm.ReplicateFn != nil {
go fs.filer.Dlm.ReplicateFn(backup, lock.Key, lock.ExpiredAtNs, lock.Token, lock.PrevToken, lock.Owner, lock.Generation, lock.Seq, false)
}
} else if backup == fs.option.Host {
if !lock.IsBackup {
// We were primary, now we're backup → transfer to new primary first, then demote
glog.V(0).Infof("DLM: transferring primary lock %s to new primary=%s", lock.Key, primary)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err := pb.WithFilerClient(false, 0, primary, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
_, err := client.TransferLocks(ctx, &filer_pb.TransferLocksRequest{
Locks: []*filer_pb.Lock{
{
Name: lock.Key,
RenewToken: lock.Token,
PrevRenewToken: lock.PrevToken,
ExpiredAtNs: lock.ExpiredAtNs,
Owner: lock.Owner,
Generation: lock.Generation,
Seq: lock.Seq,
},
},
})
return err
})
cancel()
if err != nil {
glog.Errorf("DLM: failed to transfer lock %s to new primary %s: %v, keeping as primary", lock.Key, primary, err)
} else {
// Only demote after successful handoff
fs.filer.Dlm.DemoteLock(lock.Key)
glog.V(0).Infof("DLM: demoted lock %s to backup after successful transfer", lock.Key)
}
}
// else: already backup, keep as is
} else {
// We're neither primary nor backup → transfer to primary and remove locally
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err := pb.WithFilerClient(false, 0, primary, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
_, err := client.TransferLocks(ctx, &filer_pb.TransferLocksRequest{
Locks: []*filer_pb.Lock{
{
Name: lock.Key,
RenewToken: lock.Token,
PrevRenewToken: lock.PrevToken,
ExpiredAtNs: lock.ExpiredAtNs,
Owner: lock.Owner,
Generation: lock.Generation,
Seq: lock.Seq,
},
},
})
return err
})
cancel()
if err != nil {
glog.Errorf("DLM: failed to transfer lock %s to %s: %v", lock.Key, primary, err)
} else {
fs.filer.Dlm.RemoveBackupLock(lock.Key)
}
}
}
}
// SetupDlmReplication sets up the replication callback for the DLM.
// Called during filer server initialization.
func (fs *FilerServer) SetupDlmReplication() {
fs.filer.Dlm.ReplicateFn = func(server pb.ServerAddress, key string, expiredAtNs int64, token string, prevToken string, owner string, generation int64, seq int64, isUnlock bool) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := pb.WithFilerClient(false, 0, server, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
_, err := client.ReplicateLock(ctx, &filer_pb.ReplicateLockRequest{
Name: key,
RenewToken: token,
PrevRenewToken: prevToken,
ExpiredAtNs: expiredAtNs,
Owner: owner,
Generation: generation,
Seq: seq,
IsUnlock: isUnlock,
})
return err
})
if err != nil {
glog.Warningf("DLM: failed to replicate lock %s to %s: %v", key, server, err)
}
}
}