Files
seaweedfs/weed/mount/posix_file_lock_test.go
T
Chris Lu 31e5e0dee2 fix(mount): keep async flush when LockOwner has no POSIX locks (#9300)
FlushIn.LockOwner is populated by the kernel for any fd that may have
participated in locking, not only when locks were actually taken. The
previous Flush logic treated any non-zero LockOwner as a closing lock
holder and forced a synchronous flush, which silently disabled the
writebackCache async-flush path (introduced in #8727) for most
ordinary close() calls.

Consult the POSIX lock table before forcing sync: only owners that
currently hold a non-flock byte-range lock need the synchronous path
to coordinate with blocked SetLkw waiters. Other closes go async as
intended.
2026-05-01 19:51:27 -07:00

851 lines
26 KiB
Go

package mount
import (
"math"
"runtime"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
func TestNonOverlappingLocksFromDifferentOwners(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
if s1 != fuse.OK {
t.Fatalf("expected OK, got %v", s1)
}
s2 := plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
if s2 != fuse.OK {
t.Fatalf("expected OK, got %v", s2)
}
}
func TestOverlappingReadLocksFromDifferentOwners(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
if s1 != fuse.OK {
t.Fatalf("expected OK, got %v", s1)
}
s2 := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
if s2 != fuse.OK {
t.Fatalf("expected OK, got %v", s2)
}
}
func TestOverlappingWriteReadConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
if s != fuse.EAGAIN {
t.Fatalf("expected EAGAIN, got %v", s)
}
}
func TestOverlappingWriteWriteConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
s := plt.SetLk(inode, lockRange{Start: 50, End: 149, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
if s != fuse.EAGAIN {
t.Fatalf("expected EAGAIN, got %v", s)
}
}
func TestSameOwnerUpgradeReadToWrite(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
if s != fuse.OK {
t.Fatalf("expected OK for same-owner upgrade, got %v", s)
}
// Verify the lock is now a write lock.
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, &out)
if out.Lk.Typ != syscall.F_WRLCK {
t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ)
}
}
func TestSameOwnerDowngradeWriteToRead(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
if s != fuse.OK {
t.Fatalf("expected OK for same-owner downgrade, got %v", s)
}
// Another owner should now be able to get a read lock.
s2 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
if s2 != fuse.OK {
t.Fatalf("expected OK for shared read lock, got %v", s2)
}
}
func TestLockCoalescing(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 9, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 10, End: 19, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
il := plt.getInodeLocks(inode)
il.mu.Lock()
ownerLocks := 0
for _, lk := range il.locks {
if lk.Owner == 1 {
ownerLocks++
if lk.Start != 0 || lk.End != 19 {
t.Errorf("expected coalesced lock [0,19], got [%d,%d]", lk.Start, lk.End)
}
}
}
il.mu.Unlock()
if ownerLocks != 1 {
t.Fatalf("expected 1 coalesced lock, got %d", ownerLocks)
}
}
func TestLockSplitting(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
// Unlock the middle portion.
plt.SetLk(inode, lockRange{Start: 40, End: 59, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
il := plt.getInodeLocks(inode)
il.mu.Lock()
ownerLocks := 0
for _, lk := range il.locks {
if lk.Owner == 1 {
ownerLocks++
}
}
if ownerLocks != 2 {
il.mu.Unlock()
t.Fatalf("expected 2 locks after split, got %d", ownerLocks)
}
// Check the ranges.
if il.locks[0].Start != 0 || il.locks[0].End != 39 {
t.Errorf("expected left lock [0,39], got [%d,%d]", il.locks[0].Start, il.locks[0].End)
}
if il.locks[1].Start != 60 || il.locks[1].End != 99 {
t.Errorf("expected right lock [60,99], got [%d,%d]", il.locks[1].Start, il.locks[1].End)
}
il.mu.Unlock()
}
func TestGetLkConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out)
if out.Lk.Typ != syscall.F_WRLCK {
t.Fatalf("expected conflicting write lock, got type %d", out.Lk.Typ)
}
if out.Lk.Pid != 10 {
t.Fatalf("expected holder PID 10, got %d", out.Lk.Pid)
}
if out.Lk.Start != 10 || out.Lk.End != 50 {
t.Fatalf("expected conflict [10,50], got [%d,%d]", out.Lk.Start, out.Lk.End)
}
}
func TestGetLkNoConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 10, End: 50, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 30, End: 70, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20}, &out)
if out.Lk.Typ != syscall.F_UNLCK {
t.Fatalf("expected F_UNLCK (no conflict), got type %d", out.Lk.Typ)
}
}
func TestGetLkSameOwnerNoConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10}, &out)
if out.Lk.Typ != syscall.F_UNLCK {
t.Fatalf("same owner should not conflict with itself, got type %d", out.Lk.Typ)
}
}
func TestReleaseOwner(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 49, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 50, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
plt.ReleaseOwner(inode, 1)
// Owner 1's locks should be gone.
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
if out.Lk.Typ != syscall.F_UNLCK {
t.Fatalf("expected no conflict after ReleaseOwner, got type %d", out.Lk.Typ)
}
// Owner 2's lock should still exist.
plt.GetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
if out.Lk.Typ != syscall.F_RDLCK {
t.Fatalf("expected owner 2's read lock to remain, got type %d", out.Lk.Typ)
}
}
func TestDifferentLockKindsDoNotConflict(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
s1 := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
if s1 != fuse.OK {
t.Fatalf("expected POSIX lock OK, got %v", s1)
}
s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true})
if s2 != fuse.OK {
t.Fatalf("expected flock lock OK in separate namespace, got %v", s2)
}
}
func TestReleasePosixOwnerReleasesPosixLocksAndWakesWaiters(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
done := make(chan fuse.Status, 1)
go func() {
cancel := make(chan struct{})
done <- plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
}()
time.Sleep(50 * time.Millisecond)
plt.ReleasePosixOwner(inode, 1)
select {
case s := <-done:
if s != fuse.OK {
t.Fatalf("expected OK after ReleasePosixOwner, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("SetLkw did not unblock after ReleasePosixOwner")
}
}
func TestReleasePosixOwnerDoesNotReleaseFlockLocks(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10, IsFlock: true})
plt.ReleasePosixOwner(inode, 1)
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20, IsFlock: true}, &out)
if out.Lk.Typ != syscall.F_WRLCK {
t.Fatalf("expected flock lock to remain after ReleasePosixOwner, got type %d", out.Lk.Typ)
}
}
func TestHasPosixOwnerIgnoresMissingOwnerAndFlock(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
if plt.HasPosixOwner(inode, 1) {
t.Fatal("missing owner should not be reported as holding POSIX locks")
}
if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10, IsFlock: true}); s != fuse.OK {
t.Fatalf("set flock: %v", s)
}
if plt.HasPosixOwner(inode, 1) {
t.Fatal("flock owner should not be reported as a POSIX lock owner")
}
if s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}); s != fuse.OK {
t.Fatalf("set POSIX lock: %v", s)
}
if !plt.HasPosixOwner(inode, 2) {
t.Fatal("POSIX lock owner was not reported")
}
if plt.HasPosixOwner(inode, 0) {
t.Fatal("zero owner should not be reported")
}
}
func TestWakeEligibleWaitersKeepsInodeUntilWakeRefReleased(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
il := plt.getOrCreateInodeLocks(inode)
waiter := &lockWaiter{
requested: lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20},
ch: make(chan struct{}),
}
il.mu.Lock()
il.waiters = append(il.waiters, waiter)
il.mu.Unlock()
plt.releaseMatching(inode, func(lockRange) bool { return false })
select {
case <-waiter.ch:
// Expected.
default:
t.Fatal("expected waiter to be woken")
}
plt.mu.Lock()
_, exists := plt.inodes[inode]
plt.mu.Unlock()
if !exists {
t.Fatal("inodeLocks should remain while a woken waiter still holds a wake ref")
}
il.mu.Lock()
releaseWakeRef(il, waiter)
il.mu.Unlock()
plt.maybeCleanupInode(inode, il)
plt.mu.Lock()
_, exists = plt.inodes[inode]
plt.mu.Unlock()
if exists {
t.Fatal("inodeLocks should be cleaned up after the final wake ref is released")
}
}
func TestReleaseFlockOwnerDoesNotReleasePosixLocks(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 10, IsFlock: true})
plt.ReleaseFlockOwner(inode, 2)
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, &out)
if out.Lk.Typ != syscall.F_WRLCK {
t.Fatalf("expected POSIX lock to remain after ReleaseFlockOwner, got type %d", out.Lk.Typ)
}
plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40, IsFlock: true}, &out)
if out.Lk.Typ != syscall.F_UNLCK {
t.Fatalf("expected flock lock to be removed after ReleaseFlockOwner, got type %d", out.Lk.Typ)
}
}
func TestReleaseOwnerWakesWaiters(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
done := make(chan fuse.Status, 1)
go func() {
cancel := make(chan struct{})
s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
done <- s
}()
// Give the goroutine time to block.
time.Sleep(50 * time.Millisecond)
plt.ReleaseOwner(inode, 1)
select {
case s := <-done:
if s != fuse.OK {
t.Fatalf("expected OK after ReleaseOwner woke waiter, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("SetLkw did not unblock after ReleaseOwner")
}
}
func TestSetLkwBlocksAndSucceeds(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
done := make(chan fuse.Status, 1)
go func() {
cancel := make(chan struct{})
s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
done <- s
}()
// Give the goroutine time to block.
time.Sleep(50 * time.Millisecond)
// Release the conflicting lock.
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
select {
case s := <-done:
if s != fuse.OK {
t.Fatalf("expected OK, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("SetLkw did not unblock after conflicting lock was released")
}
}
func TestSetLkwCancellation(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
cancel := make(chan struct{})
done := make(chan fuse.Status, 1)
go func() {
s := plt.SetLkw(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20}, cancel)
done <- s
}()
// Give the goroutine time to block.
time.Sleep(50 * time.Millisecond)
close(cancel)
select {
case s := <-done:
if s != fuse.EINTR {
t.Fatalf("expected EINTR on cancel, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("SetLkw did not unblock after cancel")
}
}
func TestWholeFileLock(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Simulate flock() — whole-file exclusive lock.
s1 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
if s1 != fuse.OK {
t.Fatalf("expected OK, got %v", s1)
}
// Second owner should be blocked.
s2 := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
if s2 != fuse.EAGAIN {
t.Fatalf("expected EAGAIN, got %v", s2)
}
// Even a partial overlap should fail.
s3 := plt.SetLk(inode, lockRange{Start: 100, End: 200, Typ: syscall.F_RDLCK, Owner: 2, Pid: 20})
if s3 != fuse.EAGAIN {
t.Fatalf("expected EAGAIN for partial overlap with whole-file lock, got %v", s3)
}
}
func TestUnlockNoExistingLocks(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Unlock on an inode with no locks should succeed silently.
s := plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
if s != fuse.OK {
t.Fatalf("expected OK for unlock with no existing locks, got %v", s)
}
}
func TestMultipleInodesIndependent(t *testing.T) {
plt := NewPosixLockTable()
// Write lock on inode 1 should not affect inode 2.
plt.SetLk(1, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
s := plt.SetLk(2, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
if s != fuse.OK {
t.Fatalf("locks on different inodes should be independent, got %v", s)
}
}
func TestMemoryCleanup(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.ReleaseOwner(inode, 1)
plt.mu.Lock()
_, exists := plt.inodes[inode]
plt.mu.Unlock()
if exists {
t.Fatal("expected inode entry to be cleaned up after all locks released")
}
}
func TestSelectiveWaking(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Owner 1 holds write lock on [0, 99], owner 2 holds write lock on [200, 299].
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_WRLCK, Owner: 2, Pid: 20})
// Owner 3 waits for [50, 60] (blocked by owner 1).
done3 := make(chan fuse.Status, 1)
go func() {
cancel := make(chan struct{})
s := plt.SetLkw(inode, lockRange{Start: 50, End: 60, Typ: syscall.F_WRLCK, Owner: 3, Pid: 30}, cancel)
done3 <- s
}()
// Owner 4 waits for [250, 260] (blocked by owner 2).
done4 := make(chan fuse.Status, 1)
go func() {
cancel := make(chan struct{})
s := plt.SetLkw(inode, lockRange{Start: 250, End: 260, Typ: syscall.F_WRLCK, Owner: 4, Pid: 40}, cancel)
done4 <- s
}()
time.Sleep(50 * time.Millisecond)
// Release owner 1's lock. Only owner 3 should be woken; owner 4 is still blocked.
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_UNLCK, Owner: 1, Pid: 10})
select {
case s := <-done3:
if s != fuse.OK {
t.Fatalf("expected OK for owner 3, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("owner 3 was not woken after owner 1 released")
}
// Owner 4 should still be blocked.
select {
case s := <-done4:
t.Fatalf("owner 4 should still be blocked, but got %v", s)
case <-time.After(100 * time.Millisecond):
// Expected — still blocked.
}
// Now release owner 2's lock. Owner 4 should wake.
plt.SetLk(inode, lockRange{Start: 200, End: 299, Typ: syscall.F_UNLCK, Owner: 2, Pid: 20})
select {
case s := <-done4:
if s != fuse.OK {
t.Fatalf("expected OK for owner 4, got %v", s)
}
case <-time.After(2 * time.Second):
t.Fatal("owner 4 was not woken after owner 2 released")
}
}
func TestSameOwnerReplaceDifferentType(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Lock [0, 99] as write.
plt.SetLk(inode, lockRange{Start: 0, End: 99, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
// Replace middle portion [30, 60] with read lock.
plt.SetLk(inode, lockRange{Start: 30, End: 60, Typ: syscall.F_RDLCK, Owner: 1, Pid: 10})
il := plt.getInodeLocks(inode)
il.mu.Lock()
defer il.mu.Unlock()
// Should have 3 locks: write [0,29], read [30,60], write [61,99].
if len(il.locks) != 3 {
t.Fatalf("expected 3 locks after partial type change, got %d", len(il.locks))
}
if il.locks[0].Typ != syscall.F_WRLCK || il.locks[0].Start != 0 || il.locks[0].End != 29 {
t.Errorf("expected write [0,29], got type=%d [%d,%d]", il.locks[0].Typ, il.locks[0].Start, il.locks[0].End)
}
if il.locks[1].Typ != syscall.F_RDLCK || il.locks[1].Start != 30 || il.locks[1].End != 60 {
t.Errorf("expected read [30,60], got type=%d [%d,%d]", il.locks[1].Typ, il.locks[1].Start, il.locks[1].End)
}
if il.locks[2].Typ != syscall.F_WRLCK || il.locks[2].Start != 61 || il.locks[2].End != 99 {
t.Errorf("expected write [61,99], got type=%d [%d,%d]", il.locks[2].Typ, il.locks[2].Start, il.locks[2].End)
}
}
func TestNonAdjacentRangesNotCoalesced(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Lock [5, MaxUint64] then [0, 2] — gap at [3,4] must prevent coalescing.
plt.SetLk(inode, lockRange{Start: 5, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
s := plt.SetLk(inode, lockRange{Start: 0, End: 2, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
if s != fuse.OK {
t.Fatalf("expected OK, got %v", s)
}
il := plt.getInodeLocks(inode)
il.mu.Lock()
defer il.mu.Unlock()
if len(il.locks) != 2 {
t.Fatalf("expected 2 separate locks (gap [3,4] prevents coalescing), got %d", len(il.locks))
}
if il.locks[0].Start != 0 || il.locks[0].End != 2 {
t.Errorf("expected first lock [0,2], got [%d,%d]", il.locks[0].Start, il.locks[0].End)
}
if il.locks[1].Start != 5 || il.locks[1].End != math.MaxUint64 {
t.Errorf("expected second lock [5,MaxUint64], got [%d,%d]", il.locks[1].Start, il.locks[1].End)
}
}
func TestAdjacencyNoOverflowAtMaxUint64(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(1)
// Lock to EOF (End = MaxUint64), then lock [0, 0] same type.
// Without the overflow guard, MaxUint64+1 wraps to 0, falsely merging.
plt.SetLk(inode, lockRange{Start: 100, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
plt.SetLk(inode, lockRange{Start: 0, End: 0, Typ: syscall.F_WRLCK, Owner: 1, Pid: 10})
il := plt.getInodeLocks(inode)
il.mu.Lock()
defer il.mu.Unlock()
// Should remain 2 separate locks, not merged.
ownerLocks := 0
for _, lk := range il.locks {
if lk.Owner == 1 {
ownerLocks++
}
}
if ownerLocks != 2 {
t.Fatalf("expected 2 separate locks (no overflow merge), got %d", ownerLocks)
}
}
// TestSetLkRetriesPastDeadInodeLocks deterministically exercises the
// getOrCreateInodeLocks vs maybeCleanupInode race: a caller holding a
// pointer to an inodeLocks that is concurrently marked dead must refetch
// from the map instead of mutating the orphaned instance (which would be
// invisible to subsequent callers and let two exclusive flock holders
// coexist). The test bypasses scheduling by hand-installing a dead il into
// the table and asserting that the next SetLk routes around it.
func TestSetLkRetriesPastDeadInodeLocks(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(42)
// Acquire and release a lock so maybeCleanupInode marks the il dead and
// removes it from the map.
lock := lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, IsFlock: true}
if s := plt.SetLk(inode, lock); s != fuse.OK {
t.Fatalf("prime SetLk: got %v", s)
}
dead := plt.getInodeLocks(inode)
unlock := lock
unlock.Typ = syscall.F_UNLCK
if s := plt.SetLk(inode, unlock); s != fuse.OK {
t.Fatalf("prime unlock: got %v", s)
}
if !dead.dead {
t.Fatal("expected il to be marked dead after unlock+cleanup")
}
plt.mu.Lock()
_, stillMapped := plt.inodes[inode]
plt.mu.Unlock()
if stillMapped {
t.Fatal("expected the dead il to be removed from the map")
}
// Simulate the race: the next caller's getOrCreateInodeLocks races with
// the cleanup and ends up holding a pointer to the dead il. We force that
// state by re-publishing `dead` into the map.
plt.mu.Lock()
plt.inodes[inode] = dead
plt.mu.Unlock()
// SetLk must notice dead, refetch, and install the new lock in a fresh il.
if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 2, IsFlock: true}); s != fuse.OK {
t.Fatalf("SetLk after dead: got %v", s)
}
dead.mu.Lock()
if n := len(dead.locks); n != 0 {
t.Fatalf("dead il should not have accepted the insert, found %d locks", n)
}
dead.mu.Unlock()
plt.mu.Lock()
live := plt.inodes[inode]
plt.mu.Unlock()
if live == nil || live == dead {
t.Fatalf("expected a fresh live il, got %v", live)
}
// A conflicting owner must see the new lock and be rejected.
if s := plt.SetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 3, IsFlock: true}); s != fuse.EAGAIN {
t.Fatalf("second owner should conflict with owner 2, got %v", s)
}
// GetLk must report the conflict as well: without the dead-recheck the
// GetLk path would answer F_UNLCK off the orphaned il.
var out fuse.LkOut
plt.GetLk(inode, lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 4, IsFlock: true}, &out)
if out.Lk.Typ != syscall.F_WRLCK {
t.Fatalf("GetLk should report the live conflict, got Typ=%d", out.Lk.Typ)
}
}
// TestGetInodeLocksEvictsDeadEntry verifies that a dead inodeLocks which
// somehow ends up in the map (e.g. through a future refactor that reorders
// delete and dead=true) is dropped on read so callers never observe one.
// This is the backstop that lets GetLk's and SetLk's retry loops terminate.
func TestGetInodeLocksEvictsDeadEntry(t *testing.T) {
plt := NewPosixLockTable()
inode := uint64(42)
lock := lockRange{Start: 0, End: math.MaxUint64, Typ: syscall.F_WRLCK, Owner: 1, IsFlock: true}
if s := plt.SetLk(inode, lock); s != fuse.OK {
t.Fatalf("prime SetLk: got %v", s)
}
dead := plt.getInodeLocks(inode)
unlock := lock
unlock.Typ = syscall.F_UNLCK
if s := plt.SetLk(inode, unlock); s != fuse.OK {
t.Fatalf("prime unlock: got %v", s)
}
if !dead.dead {
t.Fatal("expected dead after cleanup")
}
// Force the broken state that production cannot reach but tests and
// future refactors might: dead entry still in the map.
plt.mu.Lock()
plt.inodes[inode] = dead
plt.mu.Unlock()
if il := plt.getInodeLocks(inode); il != nil {
t.Fatalf("getInodeLocks should drop a dead map entry, got %p", il)
}
plt.mu.Lock()
_, stillMapped := plt.inodes[inode]
plt.mu.Unlock()
if stillMapped {
t.Fatal("expected dead entry to be removed from the map")
}
// getOrCreateInodeLocks must also self-heal (replace the dead entry with
// a fresh live one) so SetLk's retry path cannot spin.
plt.mu.Lock()
plt.inodes[inode] = dead
plt.mu.Unlock()
fresh := plt.getOrCreateInodeLocks(inode)
if fresh == dead {
t.Fatal("getOrCreateInodeLocks should not return a dead entry")
}
if fresh.dead {
t.Fatal("fresh entry should not be dead")
}
}
// TestConcurrentFlockChurnPreservesMutualExclusion is a stress companion to
// the deterministic tests above. It uses a Swap+CAS detector that flags
// overlap at two points (on acquire and on release), so a second granted
// holder is caught even if it sneaks in after the first goroutine's claim
// but before its release. 16 goroutines churn whole-file exclusive flock on
// one inode; with the race the detector fires hundreds of times per run,
// with the fix it stays at zero.
func TestConcurrentFlockChurnPreservesMutualExclusion(t *testing.T) {
plt := NewPosixLockTable()
const (
inode = uint64(42)
numWorkers = 16
iterations = 500
)
var (
wg sync.WaitGroup
holder atomic.Int64 // 0 = nobody; otherwise = holder's claim token
overlapSeen atomic.Int32
)
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
owner := uint64(100 + id)
lock := lockRange{
Start: 0,
End: math.MaxUint64,
Typ: syscall.F_WRLCK,
Owner: owner,
Pid: uint32(id + 1),
IsFlock: true,
}
unlock := lock
unlock.Typ = syscall.F_UNLCK
token := int64(id + 1)
for i := 0; i < iterations; i++ {
// SetLk(WRLCK) may only return OK (granted) or EAGAIN
// (conflict); anything else indicates a bug and the test
// must fail rather than spin. Use Errorf + return because
// Fatalf is not safe from a non-test goroutine.
for {
s := plt.SetLk(inode, lock)
if s == fuse.OK {
break
}
if s != fuse.EAGAIN {
t.Errorf("worker %d iter %d: unexpected SetLk(WRLCK) status %v", id, i, s)
return
}
runtime.Gosched()
}
// Claim the slot. If Swap observes a non-zero predecessor,
// another goroutine already believes it holds the lock.
if prev := holder.Swap(token); prev != 0 {
overlapSeen.Add(1)
}
// Widen the window so a concurrently-granted peer has a
// chance to race into its own Swap before we release.
runtime.Gosched()
runtime.Gosched()
// Release the slot. If CAS fails someone else overwrote our
// claim, which only happens when two holders raced.
if !holder.CompareAndSwap(token, 0) {
overlapSeen.Add(1)
}
if s := plt.SetLk(inode, unlock); s != fuse.OK {
t.Errorf("worker %d iter %d: unexpected SetLk(UNLCK) status %v", id, i, s)
return
}
}
}(w)
}
wg.Wait()
if n := overlapSeen.Load(); n != 0 {
t.Fatalf("flock overlap detected %d times: two owners simultaneously granted the same exclusive lock", n)
}
}