ollie/cmd/toolsrv/internal/server/pathlock_test.go

157 lines
3.4 KiB
Go

package server
import (
"sync"
"sync/atomic"
"testing"
"time"
)
func TestPathLock_ReadNeverBlocks(t *testing.T) {
var tbl pathLockTable
// Multiple concurrent reads should never block each other.
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
release := tbl.acquire("read", "/foo.go")
time.Sleep(1 * time.Millisecond)
release()
}()
}
wg.Wait()
}
func TestPathLock_WritesSerializeOnSamePath(t *testing.T) {
var tbl pathLockTable
var counter atomic.Int32
var maxConcurrent atomic.Int32
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
release := tbl.acquire("write", "/foo.go")
cur := counter.Add(1)
if cur > maxConcurrent.Load() {
maxConcurrent.Store(cur)
}
time.Sleep(5 * time.Millisecond)
counter.Add(-1)
release()
}()
}
wg.Wait()
if maxConcurrent.Load() != 1 {
t.Errorf("expected max concurrency 1 for same path writes, got %d", maxConcurrent.Load())
}
}
func TestPathLock_WritesParallelOnDifferentPaths(t *testing.T) {
var tbl pathLockTable
var counter atomic.Int32
var maxConcurrent atomic.Int32
var wg sync.WaitGroup
paths := []string{"/a.go", "/b.go", "/c.go", "/d.go", "/e.go"}
for _, p := range paths {
wg.Add(1)
go func(path string) {
defer wg.Done()
release := tbl.acquire("write", path)
cur := counter.Add(1)
for {
mc := maxConcurrent.Load()
if cur <= mc || maxConcurrent.CompareAndSwap(mc, cur) {
break
}
}
time.Sleep(10 * time.Millisecond)
counter.Add(-1)
release()
}(p)
}
wg.Wait()
if maxConcurrent.Load() < 2 {
t.Errorf("expected parallel writes on different paths, got max concurrency %d", maxConcurrent.Load())
}
}
func TestPathLock_GlobalBlocksWrites(t *testing.T) {
var tbl pathLockTable
var sequence []string
var mu sync.Mutex
record := func(s string) {
mu.Lock()
sequence = append(sequence, s)
mu.Unlock()
}
// Start a global lock holder.
globalReady := make(chan struct{})
globalDone := make(chan struct{})
go func() {
release := tbl.acquire("global", "")
close(globalReady)
time.Sleep(20 * time.Millisecond)
record("global-done")
release()
close(globalDone)
}()
<-globalReady
// Give the global lock time to be fully acquired.
time.Sleep(2 * time.Millisecond)
// Start a write — should block until global is done.
writeDone := make(chan struct{})
go func() {
release := tbl.acquire("write", "/foo.go")
record("write-done")
release()
close(writeDone)
}()
<-globalDone
<-writeDone
mu.Lock()
defer mu.Unlock()
if len(sequence) < 2 || sequence[0] != "global-done" || sequence[1] != "write-done" {
t.Errorf("expected global to complete before write, got %v", sequence)
}
}
func TestPathLock_WriteNoPathFallsToGlobal(t *testing.T) {
var tbl pathLockTable
var counter atomic.Int32
var maxConcurrent atomic.Int32
var wg sync.WaitGroup
// Multiple writes with empty path should serialize (fall through to global).
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
release := tbl.acquire("write", "")
cur := counter.Add(1)
if cur > maxConcurrent.Load() {
maxConcurrent.Store(cur)
}
time.Sleep(5 * time.Millisecond)
counter.Add(-1)
release()
}()
}
wg.Wait()
if maxConcurrent.Load() != 1 {
t.Errorf("expected max concurrency 1 for write with no path, got %d", maxConcurrent.Load())
}
}