Make repodata coalescing tests deterministic (#177)
The repodata coalescing tests released the gated build after a fixed 50ms sleep, so a goroutine stalled past that window under -race could miss the in-flight singleflight call, trigger a second build and flake the test. - add sfWaiters, counting goroutines parked in singleflight Do via runtime.Stack - release the gate only once every follower is parked on the in-flight call - drop the trailing sleep from waitFor Reviewed-on: #177 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #177.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -206,18 +207,35 @@ func TestCachedRepodataEvictsIdleRemotes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitFor polls until cond holds, then gives goroutines that passed the
|
|
||||||
// fingerprint a moment to reach the singleflight call.
|
|
||||||
func waitFor(t *testing.T, cond func() bool) {
|
func waitFor(t *testing.T, cond func() bool) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
deadline := time.Now().Add(5 * time.Second)
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
for !cond() {
|
for !cond() {
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
t.Fatal("condition not reached")
|
t.Fatal("condition not reached")
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
}
|
}
|
||||||
time.Sleep(50 * time.Millisecond)
|
}
|
||||||
|
|
||||||
|
// sfWaiters counts goroutines parked on an in-flight singleflight call.
|
||||||
|
func sfWaiters() int {
|
||||||
|
buf := make([]byte, 1<<20)
|
||||||
|
for {
|
||||||
|
n := runtime.Stack(buf, true)
|
||||||
|
if n < len(buf) {
|
||||||
|
buf = buf[:n]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = make([]byte, 2*len(buf))
|
||||||
|
}
|
||||||
|
waiters := 0
|
||||||
|
for _, g := range strings.Split(string(buf), "\n\n") {
|
||||||
|
if strings.Contains(g, "sync.(*WaitGroup).Wait") && strings.Contains(g, "singleflight.(*Group).Do(") {
|
||||||
|
waiters++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return waiters
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCachedRepodataCoalescesConcurrentBuilds(t *testing.T) {
|
func TestCachedRepodataCoalescesConcurrentBuilds(t *testing.T) {
|
||||||
@@ -234,7 +252,7 @@ func TestCachedRepodataCoalescesConcurrentBuilds(t *testing.T) {
|
|||||||
codes[i] = serveReq(t, p, remote, store, httptest.NewRequest(http.MethodGet, "/x", nil), repodataPaths[i%len(repodataPaths)]).Code
|
codes[i] = serveReq(t, p, remote, store, httptest.NewRequest(http.MethodGet, "/x", nil), repodataPaths[i%len(repodataPaths)]).Code
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
waitFor(t, func() bool { return store.fpCount() == n })
|
waitFor(t, func() bool { return store.listCount() == 1 && sfWaiters() == n-1 })
|
||||||
close(gate)
|
close(gate)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
for i, c := range codes {
|
for i, c := range codes {
|
||||||
@@ -261,7 +279,7 @@ func TestCachedRepodataLeaderCancelDoesNotFailWaiters(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
results <- serveReq(t, p, remote, store, httptest.NewRequest(http.MethodGet, "/x", nil), "repodata/repomd.xml").Code
|
results <- serveReq(t, p, remote, store, httptest.NewRequest(http.MethodGet, "/x", nil), "repodata/repomd.xml").Code
|
||||||
}()
|
}()
|
||||||
waitFor(t, func() bool { return store.fpCount() == 2 })
|
waitFor(t, func() bool { return sfWaiters() == 1 })
|
||||||
cancelLeader()
|
cancelLeader()
|
||||||
close(gate)
|
close(gate)
|
||||||
for range 2 {
|
for range 2 {
|
||||||
|
|||||||
Reference in New Issue
Block a user