check / check (push) Failing after 0s
Accumulating milestone branch. One squashed commit per closed issue; `next` is kept green and mergeable to `main` at any time without notice. Landed so far: - `chore: update golangci-lint to v2.12.2 with canonical config` (#54) — canonical v2-schema `.golangci.yml`, pins bumped in `Dockerfile` and `script/bootstrap`, tree at `0 issues.`. Three behaviour deltas are recorded in that PR's body: `Cache.StoreVariant` takes a context, `MetadataStorage.Store` no longer leaks temp files on failure, and the `signing_key` too-short error text gained a `value too short:` prefix. Sequencing for the milestone is tracked in #103. Reviewed-on: #105 Co-authored-by: clawbot <clawbot@noreply.example.org>
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")
|
|
}
|
|
}
|