* Add FUSE integration tests for POSIX file locking Test flock() and fcntl() advisory locks over the FUSE mount: - Exclusive and shared flock with conflict detection - flock upgrade (shared to exclusive) and release on close - fcntl F_SETLK write lock conflicts and shared read locks - fcntl F_GETLK conflict reporting on overlapping byte ranges - Non-overlapping byte-range locks held independently - F_SETLKW blocking until conflicting lock is released - Lock release on file descriptor close - Concurrent lock contention with multiple workers * Fix review feedback in POSIX lock integration tests - Assert specific EAGAIN error on fcntl lock conflicts instead of generic Error - Use O_APPEND in concurrent contention test so workers append rather than overwrite - Verify exact line count (numWorkers * writesPerWorker) after concurrent test - Check unlock error in F_SETLKW blocking test goroutine * Refactor fcntl tests to use subprocesses for inter-process semantics POSIX fcntl locks use the process's files_struct as lock owner, so all fds in the same process share the same owner and never conflict. This caused the fcntl tests to silently pass without exercising lock conflicts. Changes: - Add TestFcntlLockHelper subprocess entry point with hold/try/getlk actions - Add lockHolder with channel-based coordination (no scanner race) - Rewrite all fcntl tests to run contenders in separate subprocesses - Fix F_UNLCK int16 cast in GetLk assertion for type-safe comparison - Fix concurrent test: use non-blocking flock with retry to avoid exhausting go-fuse server reader goroutines (blocking FUSE SETLKW can starve unlock request processing, causing deadlock) flock tests remain same-process since flock uses per-struct-file owners. * Fix misleading comment and error handling in lock test subprocess - Fix comment: tryLockInSubprocess tests a subprocess, not the test process - Distinguish EAGAIN/EACCES from unexpected errors in subprocess try mode so real failures aren't silently masked as lock conflicts * Fix CI race in FcntlReleaseOnClose and increase flock retry budget - FcntlReleaseOnClose: retry lock acquisition after subprocess exits since the FUSE server may not process Release immediately - ConcurrentLockContention: increase retry limit from 500 to 3000 (5s → 30s budget) to handle CI load * separating flock and fcntl in the in-memory lock table and cleaning them up through the right release path: PID for POSIX locks, lock owner for flock * ReleasePosixOwner * weed/mount: flush before releasing posix close owner * weed/mount: keep woken lock waiters from losing inode state * test/fuse: make blocking fcntl helper state explicit * test/fuse: assert flock contention never overlaps * test/fuse: stabilize concurrent lock contention check * test/fuse: make concurrent contention writes deterministic * weed/mount: retry synchronous metadata flushes
618 lines
19 KiB
Go
618 lines
19 KiB
Go
package mount
|
|
|
|
import (
|
|
"math"
|
|
"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 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)
|
|
}
|
|
}
|