157 lines
3.4 KiB
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())
|
|
}
|
|
}
|