Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
853 lines
26 KiB
Go
853 lines
26 KiB
Go
//go:build !windows
|
|
|
|
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)
|
|
}
|
|
}
|