All checks were successful
check / check (push) Successful in 4s
Canonical v2-schema `.golangci.yml`, golangci-lint pins bumped to v2.12.2 in `Dockerfile` and `script/bootstrap`, and the tree brought to `0 issues.` under it. Three behaviour deltas: `Cache.StoreVariant` takes a context (cancelled requests skip the accounting row, recovered by reconciliation); `MetadataStorage.Store` no longer leaks `.tmp-*.json` on Write/Close/Rename failure (dead-defer bug fix); the `signing_key` too-short error text gained a `value too short:` prefix. Eviction-loop context cancellation deferred to #102.
143 lines
2.8 KiB
Go
143 lines
2.8 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) {
|
|
t.Parallel()
|
|
|
|
lock := newContentLock()
|
|
|
|
var (
|
|
active atomic.Int32
|
|
maxSeen int32
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
const goroutines = 20
|
|
|
|
wg.Add(goroutines)
|
|
|
|
for range goroutines {
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
unlock := lock.Lock("same-key")
|
|
defer unlock()
|
|
|
|
n := active.Add(1)
|
|
|
|
for {
|
|
seen := atomic.LoadInt32(&maxSeen)
|
|
if n <= seen || atomic.CompareAndSwapInt32(&maxSeen, seen, n) {
|
|
break
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Millisecond)
|
|
|
|
active.Add(-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) {
|
|
t.Parallel()
|
|
|
|
lock := newContentLock()
|
|
|
|
const goroutines = 20
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
inside atomic.Int32
|
|
reached = make(chan struct{}, goroutines)
|
|
)
|
|
|
|
wg.Add(goroutines)
|
|
|
|
release := make(chan struct{})
|
|
|
|
for i := range goroutines {
|
|
key := string(rune('a' + i))
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
unlock := lock.Lock(key)
|
|
defer unlock()
|
|
|
|
inside.Add(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 := range goroutines {
|
|
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 := inside.Load(); 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) {
|
|
t.Parallel()
|
|
|
|
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")
|
|
}
|
|
}
|