diff --git a/cmd/server/distance_lazy_index_test.go b/cmd/server/distance_lazy_index_test.go index 072b6cd3..f4c2b043 100644 --- a/cmd/server/distance_lazy_index_test.go +++ b/cmd/server/distance_lazy_index_test.go @@ -88,6 +88,14 @@ func TestDistanceConcurrentRequestsDuringBuildReturn202(t *testing.T) { if err := store.Load(); err != nil { t.Fatalf("Load(): %v", err) } + // Hold the lazy build open until every concurrent request has been served, + // so the "requests during the build window -> 202" guarantee is exercised + // deterministically. Without this the build of the tiny test DB finishes + // almost instantly, so on a fast/idle machine some requests race past it and + // get 200 — a flake, not a regression (the code only ever promised 202 for + // requests that arrive WHILE the build is in flight). + release := make(chan struct{}) + store.distanceBuildHook = func() { <-release } srv.store = store r := mux.NewRouter() srv.RegisterRoutes(r) @@ -108,6 +116,7 @@ func TestDistanceConcurrentRequestsDuringBuildReturn202(t *testing.T) { }() } wg.Wait() + close(release) // let the gated build finish if got202.Load() != N { t.Fatalf("expected all %d concurrent first-window requests to get 202; only %d did", N, got202.Load()) } diff --git a/cmd/server/paths_anchor_bias_test.go b/cmd/server/paths_anchor_bias_test.go index e1313f35..5678c404 100644 --- a/cmd/server/paths_anchor_bias_test.go +++ b/cmd/server/paths_anchor_bias_test.go @@ -78,6 +78,18 @@ func TestHandleNodePaths_AnchorBiasInconsistency_Issue1278(t *testing.T) { if err := store.Load(); err != nil { t.Fatalf("store.Load: %v", err) } + // The path-hop index that /paths reads is built in a background goroutine + // (#1008); querying before it is ready races the build and yields a + // non-deterministic membership/canonical result (the flake). Wait for + // readiness so the test asserts against the fully-built index. + pathHopDeadline := time.After(5 * time.Second) + for !store.PathHopIndexReady() { + select { + case <-pathHopDeadline: + t.Fatal("path-hop index not ready within 5s") + case <-time.After(10 * time.Millisecond): + } + } srv.store = store router := mux.NewRouter() srv.RegisterRoutes(router) diff --git a/cmd/server/store.go b/cmd/server/store.go index 4151ed94..4b4322e9 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -287,6 +287,11 @@ type PacketStore struct { distLazyBuilding bool distLazyLastBuilt time.Time distLazyLastObs int // totalObs at last build, for Δobs debounce + // distanceBuildHook, if non-nil, runs at the start of the lazy build + // goroutine (after distLazyBuilding is set, before any lock is held). Tests + // use it to hold the build open so concurrent requests deterministically + // observe the "building" window; nil (and zero overhead) in production. + distanceBuildHook func() // Cached GetNodeHashSizeInfo result — recomputed at most once every 15s hashSizeInfoMu sync.Mutex @@ -4278,6 +4283,10 @@ func (s *PacketStore) TriggerDistanceIndexBuild() { s.distLazyBuilding = true s.distLazyMu.Unlock() + if s.distanceBuildHook != nil { + s.distanceBuildHook() // test seam: hold the build window open + } + s.mu.Lock() s.buildDistanceIndex() obsAtBuild := s.totalObs