mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +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.
223 lines
6.9 KiB
Go
223 lines
6.9 KiB
Go
// Package posixlock implements the conflict, coalescing, and range-split logic
|
|
// for POSIX advisory file locks — fcntl byte-range and flock whole-file — as a
|
|
// pure per-inode lock set with no concurrency control of its own.
|
|
//
|
|
// It is the server-side authority for distributed FUSE locking: the owner filer
|
|
// for an inode holds one Set and serializes access to it under that inode's
|
|
// per-path lock, so each operation runs to completion without concurrent
|
|
// mutation. The same algorithm can back the per-mount table; blocking (SetLkw)
|
|
// and any wait queue belong to the caller, not here.
|
|
package posixlock
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
// Lock types, kept independent of the platform syscall package (whose F_RDLCK /
|
|
// F_WRLCK / F_UNLCK values differ per OS and are absent on some) so the filer
|
|
// builds everywhere. Callers map the syscall constants onto these at the edge.
|
|
// Zero is intentionally unused so a zero-value Range reads as "unset".
|
|
const (
|
|
Read uint32 = 1
|
|
Write uint32 = 2
|
|
Unlock uint32 = 3
|
|
)
|
|
|
|
// Range is one held advisory byte-range lock. Owner identity is the (Sid, Owner)
|
|
// pair: Sid is the mount session, Owner the FUSE lock owner within it, so owners
|
|
// from different mounts never alias. End is inclusive; math.MaxUint64 means EOF.
|
|
// IsFlock separates the flock and fcntl namespaces, which never conflict.
|
|
type Range struct {
|
|
Start uint64
|
|
End uint64
|
|
Type uint32
|
|
Sid uint64
|
|
Owner uint64
|
|
Pid uint32
|
|
IsFlock bool
|
|
}
|
|
|
|
func (r Range) sameOwner(o Range) bool {
|
|
return r.Sid == o.Sid && r.Owner == o.Owner
|
|
}
|
|
|
|
// Set is the authoritative set of advisory locks held on one inode. The zero
|
|
// value is an empty set. Set has no internal locking; the caller serializes it.
|
|
type Set struct {
|
|
locks []Range // sorted by Start
|
|
}
|
|
|
|
func overlap(aStart, aEnd, bStart, bEnd uint64) bool {
|
|
return aStart <= bEnd && bStart <= aEnd
|
|
}
|
|
|
|
// Conflict returns the first held lock that blocks proposed, if any. Two locks
|
|
// conflict when they share a namespace, have different owners, overlap, and at
|
|
// least one is a write lock.
|
|
func (s *Set) Conflict(proposed Range) (Range, bool) {
|
|
for _, h := range s.locks {
|
|
if h.IsFlock != proposed.IsFlock || h.sameOwner(proposed) {
|
|
continue
|
|
}
|
|
if !overlap(h.Start, h.End, proposed.Start, proposed.End) {
|
|
continue
|
|
}
|
|
if h.Type == Read && proposed.Type == Read {
|
|
continue
|
|
}
|
|
return h, true
|
|
}
|
|
return Range{}, false
|
|
}
|
|
|
|
// Acquire grants lk when it does not conflict, inserting it and coalescing the
|
|
// owner's adjacent/overlapping ranges. On conflict it returns the blocking lock
|
|
// and false, leaving the set unchanged.
|
|
func (s *Set) Acquire(lk Range) (Range, bool) {
|
|
if c, found := s.Conflict(lk); found {
|
|
return c, false
|
|
}
|
|
s.insert(lk)
|
|
return Range{}, true
|
|
}
|
|
|
|
// Grant inserts lk without a conflict check. It is for a client mirroring locks
|
|
// the server already granted (so they are conflict-free), not for arbitration.
|
|
func (s *Set) Grant(lk Range) {
|
|
s.insert(lk)
|
|
}
|
|
|
|
// insert adds lk, absorbing same-owner same-type overlaps and merging adjacent
|
|
// same-type ranges, and truncating/splitting a same-owner range of a different
|
|
// type that overlaps (an in-place type change).
|
|
func (s *Set) insert(lk Range) {
|
|
var kept []Range
|
|
for _, h := range s.locks {
|
|
if !h.sameOwner(lk) || h.IsFlock != lk.IsFlock {
|
|
kept = append(kept, h)
|
|
continue
|
|
}
|
|
if !overlap(h.Start, h.End, lk.Start, lk.End) {
|
|
// Merge only ranges that are adjacent and the same type. The
|
|
// End < MaxUint64 guards stop +1 from wrapping at EOF.
|
|
if h.Type == lk.Type && ((h.End < math.MaxUint64 && h.End+1 == lk.Start) || (lk.End < math.MaxUint64 && lk.End+1 == h.Start)) {
|
|
if h.Start < lk.Start {
|
|
lk.Start = h.Start
|
|
}
|
|
if h.End > lk.End {
|
|
lk.End = h.End
|
|
}
|
|
continue
|
|
}
|
|
kept = append(kept, h)
|
|
continue
|
|
}
|
|
if h.Type == lk.Type {
|
|
// Same type: absorb into lk by widening its range.
|
|
if h.Start < lk.Start {
|
|
lk.Start = h.Start
|
|
}
|
|
if h.End > lk.End {
|
|
lk.End = h.End
|
|
}
|
|
continue
|
|
}
|
|
// Different type: the surviving portions of h outside lk's range stay.
|
|
if h.Start < lk.Start {
|
|
left := h
|
|
left.End = lk.Start - 1
|
|
kept = append(kept, left)
|
|
}
|
|
if h.End > lk.End {
|
|
right := h
|
|
right.Start = lk.End + 1
|
|
kept = append(kept, right)
|
|
}
|
|
}
|
|
kept = append(kept, lk)
|
|
sort.Slice(kept, func(i, j int) bool { return kept[i].Start < kept[j].Start })
|
|
s.locks = kept
|
|
}
|
|
|
|
// remove drops or splits matching locks within [start,end]. A match that
|
|
// straddles the range keeps its non-overlapping head and/or tail.
|
|
func (s *Set) remove(matches func(Range) bool, start, end uint64) {
|
|
var kept []Range
|
|
for _, h := range s.locks {
|
|
if !matches(h) || !overlap(h.Start, h.End, start, end) {
|
|
kept = append(kept, h)
|
|
continue
|
|
}
|
|
if h.Start < start {
|
|
left := h
|
|
left.End = start - 1
|
|
kept = append(kept, left)
|
|
}
|
|
if h.End > end {
|
|
right := h
|
|
right.Start = end + 1
|
|
kept = append(kept, right)
|
|
}
|
|
// Fully covered: dropped.
|
|
}
|
|
s.locks = kept
|
|
}
|
|
|
|
// Release clears lk's owner's locks within lk's namespace over [lk.Start,lk.End]
|
|
// — the F_UNLCK path, which may split a straddling range.
|
|
func (s *Set) Release(lk Range) {
|
|
s.remove(func(h Range) bool {
|
|
return h.sameOwner(lk) && h.IsFlock == lk.IsFlock
|
|
}, lk.Start, lk.End)
|
|
}
|
|
|
|
// ReleaseOwner removes every lock held by (sid, owner) in both namespaces.
|
|
func (s *Set) ReleaseOwner(sid, owner uint64) {
|
|
s.remove(func(h Range) bool {
|
|
return h.Sid == sid && h.Owner == owner
|
|
}, 0, math.MaxUint64)
|
|
}
|
|
|
|
// ReleaseFlockOwner removes only the flock locks of (sid, owner) — the close-time
|
|
// path for a released file description (FUSE_RELEASE_FLOCK_UNLOCK).
|
|
func (s *Set) ReleaseFlockOwner(sid, owner uint64) {
|
|
s.remove(func(h Range) bool {
|
|
return h.IsFlock && h.Sid == sid && h.Owner == owner
|
|
}, 0, math.MaxUint64)
|
|
}
|
|
|
|
// ReleasePosixOwner removes only the fcntl locks of (sid, owner) — the close-time
|
|
// path for a flushing POSIX lock owner.
|
|
func (s *Set) ReleasePosixOwner(sid, owner uint64) {
|
|
s.remove(func(h Range) bool {
|
|
return !h.IsFlock && h.Sid == sid && h.Owner == owner
|
|
}, 0, math.MaxUint64)
|
|
}
|
|
|
|
// ReleaseSession removes every lock held by a session, reaping a mount that has
|
|
// died or disconnected (its lease expired).
|
|
func (s *Set) ReleaseSession(sid uint64) {
|
|
s.remove(func(h Range) bool { return h.Sid == sid }, 0, math.MaxUint64)
|
|
}
|
|
|
|
// HasPosix reports whether (sid, owner) holds any fcntl lock, mirroring the
|
|
// mount's flush-time check that avoids treating a lock-free flush as
|
|
// lock-sensitive.
|
|
func (s *Set) HasPosix(sid, owner uint64) bool {
|
|
for _, h := range s.locks {
|
|
if !h.IsFlock && h.Sid == sid && h.Owner == owner {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Locks returns the held locks, sorted by Start. The slice aliases internal
|
|
// state; the caller must not mutate it.
|
|
func (s *Set) Locks() []Range { return s.locks }
|
|
|
|
// Empty reports whether no locks are held, so the caller can drop the inode's
|
|
// entry from its table.
|
|
func (s *Set) Empty() bool { return len(s.locks) == 0 }
|