Introduces the keyed exclusion primitive that StoreSource and evictSourceBlob will hold across their full operation, so a store and an eviction racing on identical content bytes cannot interleave. Covered here in isolation: same-key exclusion, independence across distinct keys, and that the entry map does not grow unbounded.
134 lines
2.9 KiB
Go
134 lines
2.9 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestContentLockExcludesSameKey verifies that two goroutines locking
|
|
// the same key never run their critical sections concurrently.
|
|
func TestContentLockExcludesSameKey(t *testing.T) {
|
|
lock := newContentLock()
|
|
|
|
var (
|
|
active int32
|
|
maxSeen int32
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
const goroutines = 20
|
|
|
|
wg.Add(goroutines)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
unlock := lock.Lock("same-key")
|
|
defer unlock()
|
|
|
|
n := atomic.AddInt32(&active, 1)
|
|
|
|
for {
|
|
seen := atomic.LoadInt32(&maxSeen)
|
|
if n <= seen || atomic.CompareAndSwapInt32(&maxSeen, seen, n) {
|
|
break
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Millisecond)
|
|
|
|
atomic.AddInt32(&active, -1)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if maxSeen != 1 {
|
|
t.Errorf("max concurrent holders of the same key = %d, want 1", maxSeen)
|
|
}
|
|
}
|
|
|
|
// TestContentLockAllowsDifferentKeys verifies that locking distinct
|
|
// keys does not serialize unrelated work: all goroutines must be able
|
|
// to enter their critical sections at once, proven by every one of
|
|
// them reaching the rendezvous point before any is allowed to
|
|
// proceed.
|
|
func TestContentLockAllowsDifferentKeys(t *testing.T) {
|
|
lock := newContentLock()
|
|
|
|
const goroutines = 20
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
inside int32
|
|
reached = make(chan struct{}, goroutines)
|
|
)
|
|
|
|
wg.Add(goroutines)
|
|
|
|
release := make(chan struct{})
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
key := string(rune('a' + i))
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
unlock := lock.Lock(key)
|
|
defer unlock()
|
|
|
|
atomic.AddInt32(&inside, 1)
|
|
reached <- struct{}{}
|
|
<-release
|
|
}()
|
|
}
|
|
|
|
// Every goroutine must reach the rendezvous point (i.e. acquire its
|
|
// own key's lock) without needing any other to release first. If
|
|
// keys were incorrectly serialized onto one underlying lock, only
|
|
// one would get here and this would time out.
|
|
for i := 0; i < goroutines; i++ {
|
|
select {
|
|
case <-reached:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("only %d/%d goroutines locking distinct keys made progress; "+
|
|
"keys may be incorrectly serialized", i, goroutines)
|
|
}
|
|
}
|
|
|
|
if n := atomic.LoadInt32(&inside); n != goroutines {
|
|
t.Errorf("goroutines inside their critical section = %d, want %d", n, goroutines)
|
|
}
|
|
|
|
close(release)
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestContentLockRemovesEntryAfterUnlock verifies that the internal
|
|
// entries map does not grow without bound: once no goroutine holds or
|
|
// awaits a key, its entry is removed.
|
|
func TestContentLockRemovesEntryAfterUnlock(t *testing.T) {
|
|
lock := newContentLock()
|
|
|
|
unlock := lock.Lock("k")
|
|
|
|
lock.mu.Lock()
|
|
if _, ok := lock.entries["k"]; !ok {
|
|
lock.mu.Unlock()
|
|
t.Fatal("entry missing while lock is held")
|
|
}
|
|
lock.mu.Unlock()
|
|
|
|
unlock()
|
|
|
|
lock.mu.Lock()
|
|
defer lock.mu.Unlock()
|
|
|
|
if _, ok := lock.entries["k"]; ok {
|
|
t.Error("entry for key still present after the last holder unlocked")
|
|
}
|
|
}
|