mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 21:10:48 +02:00
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.
851 lines
26 KiB
Go
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)
|
|
}
|
|
}
|