package main import ( "sync" "testing" "time" ) // The award snapshot must be built ONCE however many callers ask at once. // // The cache lock is released before the logbook is read, so every caller that // arrives during a build used to miss the cache and start its own. Opening the // Awards panel does exactly that: a field log showed three pulls of the same // 123 615 QSOs inside ten seconds, and three copies alive together took the Go // heap from 725 MB to 2.5 GB. // // This models the same shape — a cheap cache check, a slow build, a shared // result — against the pattern awardSnapshot now uses, so the invariant is // pinned without needing a logbook. func TestSnapshotBuildsOncePerRevision(t *testing.T) { var ( cacheMu sync.Mutex buildMu sync.Mutex cached []int rev = "r1" gotRev string builds int ) get := func() []int { cacheMu.Lock() if cached != nil && gotRev == rev { defer cacheMu.Unlock() return cached } cacheMu.Unlock() buildMu.Lock() defer buildMu.Unlock() // Re-check: whoever held the build lock has just finished. cacheMu.Lock() if cached != nil && gotRev == rev { defer cacheMu.Unlock() return cached } cacheMu.Unlock() time.Sleep(50 * time.Millisecond) // the logbook read out := []int{1, 2, 3} cacheMu.Lock() builds++ cached, gotRev = out, rev cacheMu.Unlock() return out } var wg sync.WaitGroup results := make([][]int, 8) for i := range results { wg.Add(1) go func(i int) { defer wg.Done(); results[i] = get() }(i) } wg.Wait() if builds != 1 { t.Errorf("%d builds for one revision — each concurrent caller pulled the whole logbook again", builds) } for i, r := range results { if len(r) != 3 { t.Errorf("caller %d got %v", i, r) } } // A new revision must rebuild: the guard is against duplicate work, not // against a logbook that changed. cacheMu.Lock() rev = "r2" cacheMu.Unlock() get() if builds != 2 { t.Errorf("builds = %d after the revision moved, want 2 — a changed logbook must be re-read", builds) } }