mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-15 10:38:35 +02:00
* mount: renew POSIX lock leases via keepalive The mount tracks the inode keys it holds locks on and a background loop renews its session lease (KEEP_ALIVE) with each key's owner filer every 5s, within the filer's 15s TTL. A live mount is never reaped; a dead one stops renewing and owners reclaim its locks. Tracking is a superset: holds are added on grant and dropped only on owner release, so a still held lock is never under-renewed. * mount,filer: re-assert held POSIX locks via keepalive The owner filer holds POSIX advisory locks as in-memory soft state, so a key's owner change (ring rebalance) or an owner restart lost or stranded them: the new or restarted owner was blind to existing holders and would double-grant. Make the keepalive carry the mount's held lock ranges per key. The mount mirrors its own granted locks (posixOwn), and each tick re-asserts them to the key's current owner, which rebuilds that session's locks from the assertion — self -healing after a takeover or restart. The owner arbitrates re-asserted locks against other sessions so it never double-grants; a lock that lost a migration race is reported, not forced. A bare keepalive (no ranges) still just renews.
427 lines
14 KiB
Go
427 lines
14 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"math/rand/v2"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer/posixlock"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// randomPosixSid returns a process-stable, cluster-unique-enough session id for
|
|
// this mount, used to namespace lock owners so the same FUSE owner value on a
|
|
// different mount never aliases, and to scope lease-based reaping.
|
|
func randomPosixSid() uint64 {
|
|
return binary.BigEndian.Uint64(util.RandomBytes(8))
|
|
}
|
|
|
|
// Routed POSIX locking: when -dlm is set, advisory locks are serialized on the
|
|
// inode's owner filer instead of in this mount's local table, so locks are
|
|
// honored across mounts. The mount calls its filer and relies on filer-side
|
|
// forwarding to reach the owner. Blocking (SetLkw) is client-side polling — there
|
|
// is no server-side wait queue.
|
|
|
|
const (
|
|
posixLockMinBackoff = 5 * time.Millisecond
|
|
posixLockMaxBackoff = 200 * time.Millisecond
|
|
posixLockKeyPrefix = "s3.fuse.lock:"
|
|
// posixLockReleaseTimeout bounds the background unlock/release RPCs. They run
|
|
// off the syscall path (close/flush) and must not be cancelled by an
|
|
// interrupt, but a deadline keeps a slow or unreachable filer from blocking
|
|
// close indefinitely. It also bounds each keepalive RPC.
|
|
posixLockReleaseTimeout = 5 * time.Second
|
|
// posixKeepaliveInterval renews the session lease well within the filer's
|
|
// posixLockSessionTTL (15s) so a live mount is never reaped.
|
|
posixKeepaliveInterval = 5 * time.Second
|
|
)
|
|
|
|
// posixLockHint records, per inode, which owners this mount has taken fcntl locks
|
|
// for. Flush consults it to force a synchronous close and route a release without
|
|
// an RPC on every close(). It is a superset hint: an extra sync flush or a no-op
|
|
// release RPC is harmless, a missed release is not — so it is added on grant and
|
|
// cleared on close (which drops the owner's POSIX locks per POSIX semantics).
|
|
type posixLockHint struct {
|
|
mu sync.Mutex
|
|
m map[uint64]map[uint64]struct{}
|
|
}
|
|
|
|
func newPosixLockHint() *posixLockHint {
|
|
return &posixLockHint{m: make(map[uint64]map[uint64]struct{})}
|
|
}
|
|
|
|
func (h *posixLockHint) add(inode, owner uint64) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
owners := h.m[inode]
|
|
if owners == nil {
|
|
owners = make(map[uint64]struct{})
|
|
h.m[inode] = owners
|
|
}
|
|
owners[owner] = struct{}{}
|
|
}
|
|
|
|
func (h *posixLockHint) has(inode, owner uint64) bool {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
_, ok := h.m[inode][owner]
|
|
return ok
|
|
}
|
|
|
|
func (h *posixLockHint) drop(inode, owner uint64) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if owners := h.m[inode]; owners != nil {
|
|
delete(owners, owner)
|
|
if len(owners) == 0 {
|
|
delete(h.m, inode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// posixLockKeyForInode resolves a FUSE inode to its cluster-stable lock identity:
|
|
// the HardLinkId for a hardlinked file (so all names share one owner and table),
|
|
// else the path. POSIX locks are inode-scoped, so this must never be the FUSE
|
|
// NodeId (mount-local) for a multi-named inode.
|
|
func (wfs *WFS) posixLockKeyForInode(inode uint64) (string, bool) {
|
|
path, status := wfs.inodeToPath.GetPath(inode)
|
|
if status != fuse.OK {
|
|
return "", false
|
|
}
|
|
if entry, st := wfs.maybeLoadEntry(path); st == fuse.OK && entry != nil && len(entry.HardLinkId) > 0 {
|
|
return posixLockKeyPrefix + "hl:" + hex.EncodeToString(entry.HardLinkId), true
|
|
}
|
|
return posixLockKeyPrefix + string(path), true
|
|
}
|
|
|
|
func posixLockTypeToWire(typ uint32) uint32 {
|
|
switch typ {
|
|
case syscall.F_RDLCK:
|
|
return posixlock.Read
|
|
case syscall.F_WRLCK:
|
|
return posixlock.Write
|
|
default:
|
|
return posixlock.Unlock
|
|
}
|
|
}
|
|
|
|
func posixLockTypeFromWire(typ uint32) uint32 {
|
|
switch typ {
|
|
case posixlock.Read:
|
|
return syscall.F_RDLCK
|
|
case posixlock.Write:
|
|
return syscall.F_WRLCK
|
|
default:
|
|
return syscall.F_UNLCK
|
|
}
|
|
}
|
|
|
|
func (wfs *WFS) posixRangeFromLkIn(in *fuse.LkIn) posixlock.Range {
|
|
return posixlock.Range{
|
|
Start: in.Lk.Start,
|
|
End: in.Lk.End,
|
|
Type: posixLockTypeToWire(in.Lk.Typ),
|
|
Sid: wfs.posixSid,
|
|
Owner: in.Owner,
|
|
Pid: in.Lk.Pid,
|
|
IsFlock: in.LkFlags&fuse.FUSE_LK_FLOCK != 0,
|
|
}
|
|
}
|
|
|
|
// posixLockContext derives a context from the FUSE cancel channel so an
|
|
// interrupted lock syscall aborts its in-flight RPC. The caller must call the
|
|
// returned func to release the watcher goroutine.
|
|
func posixLockContext(cancel <-chan struct{}) (context.Context, context.CancelFunc) {
|
|
ctx, cancelFn := context.WithCancel(context.Background())
|
|
if cancel != nil {
|
|
go func() {
|
|
select {
|
|
case <-cancel:
|
|
cancelFn()
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
}
|
|
return ctx, cancelFn
|
|
}
|
|
|
|
func (wfs *WFS) callPosixLock(ctx context.Context, key string, op filer_pb.PosixLockOp, lk posixlock.Range) (*filer_pb.PosixLockResponse, error) {
|
|
var resp *filer_pb.PosixLockResponse
|
|
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
var e error
|
|
resp, e = client.PosixLock(ctx, &filer_pb.PosixLockRequest{
|
|
Key: key,
|
|
Op: op,
|
|
Lock: &filer_pb.PosixLockRange{
|
|
Start: lk.Start, End: lk.End, Type: lk.Type,
|
|
Sid: lk.Sid, Owner: lk.Owner, Pid: lk.Pid, IsFlock: lk.IsFlock,
|
|
},
|
|
})
|
|
return e
|
|
})
|
|
return resp, err
|
|
}
|
|
|
|
func (wfs *WFS) routedGetLk(cancel <-chan struct{}, in *fuse.LkIn, out *fuse.LkOut) fuse.Status {
|
|
key, ok := wfs.posixLockKeyForInode(in.NodeId)
|
|
if !ok {
|
|
return fuse.EINVAL
|
|
}
|
|
ctx, done := posixLockContext(cancel)
|
|
defer done()
|
|
resp, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_GET_LK, wfs.posixRangeFromLkIn(in))
|
|
if err != nil {
|
|
glog.Warningf("routed GetLk %s: %v", key, err)
|
|
return fuse.EIO
|
|
}
|
|
if resp.GetHasConflict() {
|
|
c := resp.GetConflict()
|
|
out.Lk.Start, out.Lk.End, out.Lk.Pid = c.GetStart(), c.GetEnd(), c.GetPid()
|
|
out.Lk.Typ = posixLockTypeFromWire(c.GetType())
|
|
} else {
|
|
out.Lk.Typ = syscall.F_UNLCK
|
|
}
|
|
return fuse.OK
|
|
}
|
|
|
|
func (wfs *WFS) routedSetLk(cancel <-chan struct{}, in *fuse.LkIn) fuse.Status {
|
|
key, ok := wfs.posixLockKeyForInode(in.NodeId)
|
|
if !ok {
|
|
return fuse.EINVAL
|
|
}
|
|
lk := wfs.posixRangeFromLkIn(in)
|
|
if lk.Type == posixlock.Unlock {
|
|
return wfs.routedUnlock(key, lk)
|
|
}
|
|
ctx, done := posixLockContext(cancel)
|
|
defer done()
|
|
resp, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_TRY_LOCK, lk)
|
|
if err != nil {
|
|
glog.Warningf("routed SetLk %s: %v", key, err)
|
|
return fuse.EIO
|
|
}
|
|
if !resp.GetGranted() {
|
|
return fuse.EAGAIN
|
|
}
|
|
wfs.recordPosixGrant(key, in.NodeId, lk)
|
|
return fuse.OK
|
|
}
|
|
|
|
func (wfs *WFS) routedSetLkw(cancel <-chan struct{}, in *fuse.LkIn) fuse.Status {
|
|
key, ok := wfs.posixLockKeyForInode(in.NodeId)
|
|
if !ok {
|
|
return fuse.EINVAL
|
|
}
|
|
lk := wfs.posixRangeFromLkIn(in)
|
|
if lk.Type == posixlock.Unlock {
|
|
return wfs.routedUnlock(key, lk)
|
|
}
|
|
ctx, done := posixLockContext(cancel)
|
|
defer done()
|
|
status := posixPollAcquire(cancel, func() (bool, error) {
|
|
resp, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_TRY_LOCK, lk)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return resp.GetGranted(), nil
|
|
})
|
|
if status == fuse.OK {
|
|
wfs.recordPosixGrant(key, in.NodeId, lk)
|
|
}
|
|
return status
|
|
}
|
|
|
|
// recordPosixGrant notes a newly granted lock: the flush hint (fcntl only) and
|
|
// the own-lock mirror, which the keepalive re-asserts to the key's owner so the
|
|
// lease is renewed and the owner can rebuild after a takeover or restart.
|
|
func (wfs *WFS) recordPosixGrant(key string, inode uint64, lk posixlock.Range) {
|
|
if !lk.IsFlock {
|
|
wfs.posixHint.add(inode, lk.Owner)
|
|
}
|
|
wfs.posixOwn.Track(key, lk)
|
|
}
|
|
|
|
func (wfs *WFS) routedUnlock(key string, lk posixlock.Range) fuse.Status {
|
|
// Drop the range from the mirror first so a keepalive re-assertion can't
|
|
// resurrect it; if the RPC below fails, the next re-assertion reconciles the
|
|
// owner to this (released) state anyway.
|
|
wfs.posixOwn.Unlock(key, lk)
|
|
// A release must complete even if the syscall was interrupted; cancelling it
|
|
// would leak the lock on the owner filer. Bound it so a stuck filer can't
|
|
// hang close() forever.
|
|
ctx, cancel := context.WithTimeout(context.Background(), posixLockReleaseTimeout)
|
|
defer cancel()
|
|
if _, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_UNLOCK, lk); err != nil {
|
|
glog.Warningf("routed unlock %s: %v", key, err)
|
|
return fuse.EIO
|
|
}
|
|
return fuse.OK
|
|
}
|
|
|
|
// posixPollAcquire retries try with capped, jittered backoff until it is granted,
|
|
// the cancel channel fires (EINTR), or try errors (EIO). This is the client-side
|
|
// stand-in for a server-side wait queue.
|
|
func posixPollAcquire(cancel <-chan struct{}, try func() (bool, error)) fuse.Status {
|
|
backoff := posixLockMinBackoff
|
|
for {
|
|
select {
|
|
case <-cancel:
|
|
return fuse.EINTR
|
|
default:
|
|
}
|
|
granted, err := try()
|
|
if err != nil {
|
|
return fuse.EIO
|
|
}
|
|
if granted {
|
|
return fuse.OK
|
|
}
|
|
timer := time.NewTimer(backoff + time.Duration(rand.Int64N(int64(backoff))))
|
|
select {
|
|
case <-cancel:
|
|
timer.Stop()
|
|
return fuse.EINTR
|
|
case <-timer.C:
|
|
}
|
|
if backoff < posixLockMaxBackoff {
|
|
if backoff *= 2; backoff > posixLockMaxBackoff {
|
|
backoff = posixLockMaxBackoff
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (wfs *WFS) routedReleasePosixOwner(inode, owner uint64) {
|
|
if !wfs.posixHint.has(inode, owner) {
|
|
return
|
|
}
|
|
key, ok := wfs.posixLockKeyForInode(inode)
|
|
if !ok {
|
|
return
|
|
}
|
|
wfs.posixOwn.ReleasePosixOwner(key, wfs.posixSid, owner)
|
|
ctx, cancel := context.WithTimeout(context.Background(), posixLockReleaseTimeout)
|
|
defer cancel()
|
|
if _, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_RELEASE_POSIX_OWNER, posixlock.Range{Sid: wfs.posixSid, Owner: owner}); err != nil {
|
|
// Keep the hint so a later flush retries the release; dropping it on a
|
|
// transient failure would strand the lock until the owner filer's
|
|
// session-lease reaping expires it.
|
|
glog.Warningf("routed release posix owner %s: %v", key, err)
|
|
return
|
|
}
|
|
wfs.posixHint.drop(inode, owner)
|
|
}
|
|
|
|
func (wfs *WFS) routedReleaseFlockOwner(inode, owner uint64) {
|
|
key, ok := wfs.posixLockKeyForInode(inode)
|
|
if !ok {
|
|
return
|
|
}
|
|
wfs.posixOwn.ReleaseFlockOwner(key, wfs.posixSid, owner)
|
|
ctx, cancel := context.WithTimeout(context.Background(), posixLockReleaseTimeout)
|
|
defer cancel()
|
|
if _, err := wfs.callPosixLock(ctx, key, filer_pb.PosixLockOp_RELEASE_FLOCK_OWNER, posixlock.Range{Sid: wfs.posixSid, Owner: owner}); err != nil {
|
|
glog.Warningf("routed release flock owner %s: %v", key, err)
|
|
}
|
|
}
|
|
|
|
// crossMountLocks reports whether advisory locks are routed to the inode's owner
|
|
// filer rather than served from the per-mount local table. It tracks -dlm: lock
|
|
// coordination rides the same switch as whole-file write coordination, and is
|
|
// off under writeback cache (which implies single-writer, so lockClient is nil).
|
|
func (wfs *WFS) crossMountLocks() bool {
|
|
return wfs.lockClient != nil
|
|
}
|
|
|
|
// releasePosixOwner / releaseFlockOwner / hasPosixOwner dispatch to the routed
|
|
// authority or the local table based on crossMountLocks, keeping the Flush and
|
|
// Release call sites flag-agnostic.
|
|
|
|
func (wfs *WFS) releasePosixOwner(inode, owner uint64) {
|
|
if wfs.crossMountLocks() {
|
|
wfs.routedReleasePosixOwner(inode, owner)
|
|
return
|
|
}
|
|
wfs.posixLocks.ReleasePosixOwner(inode, owner)
|
|
}
|
|
|
|
func (wfs *WFS) releaseFlockOwner(inode, owner uint64) {
|
|
if wfs.crossMountLocks() {
|
|
wfs.routedReleaseFlockOwner(inode, owner)
|
|
return
|
|
}
|
|
wfs.posixLocks.ReleaseFlockOwner(inode, owner)
|
|
}
|
|
|
|
func (wfs *WFS) hasPosixOwner(inode, owner uint64) bool {
|
|
if wfs.crossMountLocks() {
|
|
return owner != 0 && wfs.posixHint.has(inode, owner)
|
|
}
|
|
return wfs.posixLocks.HasPosixOwner(inode, owner)
|
|
}
|
|
|
|
// callPosixReassert sends the mount's held locks on key to the key's current
|
|
// owner filer as a KEEP_ALIVE, which renews the lease and lets the owner rebuild
|
|
// its in-memory state after a takeover or restart.
|
|
func (wfs *WFS) callPosixReassert(ctx context.Context, key string, locks []posixlock.Range) error {
|
|
pbLocks := make([]*filer_pb.PosixLockRange, 0, len(locks))
|
|
for _, l := range locks {
|
|
pbLocks = append(pbLocks, &filer_pb.PosixLockRange{
|
|
Start: l.Start, End: l.End, Type: l.Type,
|
|
Sid: l.Sid, Owner: l.Owner, Pid: l.Pid, IsFlock: l.IsFlock,
|
|
})
|
|
}
|
|
return wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
_, e := client.PosixLock(ctx, &filer_pb.PosixLockRequest{
|
|
Key: key,
|
|
Op: filer_pb.PosixLockOp_KEEP_ALIVE,
|
|
Lock: &filer_pb.PosixLockRange{Sid: wfs.posixSid},
|
|
Locks: pbLocks,
|
|
})
|
|
return e
|
|
})
|
|
}
|
|
|
|
// posixKeepaliveConcurrency bounds the parallel keepalive RPCs per tick. A mount
|
|
// holding locks on many inodes must renew every lease well within the filer TTL;
|
|
// dispatching them concurrently keeps the round trip from scaling with lock count.
|
|
const posixKeepaliveConcurrency = 32
|
|
|
|
// loopRenewPosixLeases re-asserts this mount's held locks to the owner filer of
|
|
// every key, which renews the session lease and rebuilds the owner's state after
|
|
// a ring change or owner restart. A dead mount stops re-asserting and the owners'
|
|
// sweepers reclaim its locks after the TTL.
|
|
func (wfs *WFS) loopRenewPosixLeases() {
|
|
ticker := time.NewTicker(posixKeepaliveInterval)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
held := wfs.posixOwn.Snapshot()
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, posixKeepaliveConcurrency)
|
|
for key, locks := range held {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
// Bound each re-assertion so a stuck filer can't block wg.Wait and
|
|
// stall the whole tick, which would let other keys' leases expire
|
|
// and get reaped.
|
|
ctx, cancel := context.WithTimeout(context.Background(), posixLockReleaseTimeout)
|
|
defer cancel()
|
|
if err := wfs.callPosixReassert(ctx, key, locks); err != nil {
|
|
glog.V(2).Infof("posix reassert %s: %v", key, err)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
}
|