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") } }