LabKit v2.8.0: httpserver.Server.{Addr,ProbeAddr}() race with Server.Start
📫 Context
LabKit v2.8.0's httpserver.Server.Start writes the bound listener fields (s.ln, s.probeLn) at labkit/v2/httpserver/server.go:241 (and surrounding lines) without explicit synchronization against concurrent reads of those fields via the public Server.Addr() and Server.ProbeAddr() accessors at server.go:206-221.
Go's -race detector flags two distinct races during the lifecycle integration tests that the S01 Step 6 composition root drives:
probeLnfield assignment vs nil-check.Startwritess.probeLnwhile a concurrent reader callsProbeAddr()and hitsif s.probeLn == nilatserver.go:217.probeLn.Addr()byte read. OnceprobeLnis non-nil,ProbeAddr()callss.probeLn.Addr(), which races withStart's ongoing field initialization on the*TCPListener.
The race surfaces when tests spawn go app.Run(ctx) and then poll srv.ProbeAddr() / srv.Addr() from the test goroutine to discover the bound ephemeral ports. Tests pass without -race; they fail under -race on every run.
Sample race trace (full output available in the AR MR thread):
WARNING: DATA RACE
Write at 0x00c000481b40 by goroutine 22:
gitlab.com/gitlab-org/labkit/v2/httpserver.(*Server).Start()
labkit/v2@v2.8.0/httpserver/server.go:241
Previous read at 0x00c000481b40 by goroutine 11:
gitlab.com/gitlab-org/labkit/v2/httpserver.(*Server).ProbeAddr()
labkit/v2@v2.8.0/httpserver/server.go:217The accessors are documented for cross-goroutine use ("Use this after starting with Addr :0 to discover the bound port" — server.go:206-211), so callers reasonably expect them to be race-safe.
🚒 Workaround in AR
S01 Step 6's lifecycle integration tests register a test-only startBarrier app.Component as the last component on the App. The barrier's Start closes a channel; LIFO start ordering guarantees its Start runs only after Server.Start has returned, and the channel close gives the test goroutine a happens-before edge against LabKit's listener-field writes. Reads of srv.Addr() / srv.ProbeAddr() from the test goroutine after <-barrier.started are therefore race-safe without polling or build-tag skipping. See cmd/artifact-registry/main_test.go (startBarrier, runApp).
🚒 Upstream resolution
Status: upstream fix merged. gitlab-org/labkit!504 (merged) merged on 2026-05-26 and shipped in LabKit v2.10.2. The fix adds an sync.RWMutex to httpserver.Server: Start writes s.ln / s.probeLn under mu.Lock() and Addr() / ProbeAddr() read under mu.RLock().
Confirm against the released source (after go mod download gitlab.com/gitlab-org/labkit/v2@v2.10.2):
// httpserver/server.go @ v2.10.2
type Server struct {
mu sync.RWMutex
// ...
}
func (s *Server) Addr() string {
s.mu.RLock()
defer s.mu.RUnlock()
// ...
}Post-merge cleanup (this issue's actionable scope)
- Bump the
gitlab.com/gitlab-org/labkit/v2pin ingo.modfromv2.8.0tov2.10.2(or the latest patch release containing the fix). Rungo mod tidy. - Run the lifecycle suite under
-race -count=10against the bumped LabKit to confirm the race no longer fires. The startBarrier is now a no-op on a fixed LabKit, so this should be a green run on the first try. - Optional cleanup (recommended once #2 is confirmed): remove the
startBarriertype, theawaitStartBarrierhelper, and the barrier registration fromrunApp/TestLifecycle_DrainTimeoutExits1incmd/artifact-registry/main_test.go. Reads ofsrv.ProbeAddr()/srv.Addr()can go back to direct calls afterapp.Startreturns. - Update the cluster-header comment in
main_test.go(currently pointing at this issue) to drop the race rationale, or remove the comment entirely. - Cross-check with issue #98 — that issue's section 1b also touches lifecycle-test wiring; coordinate if both MRs land at once.
- Close this issue.
The bump-and-cleanup is intentionally separate from gitlab-org/ops/artifact-registry!291 (S01 Step 6) to keep that MR focused. It can ride as a standalone chore MR.
🧭 Related
- gitlab-org/labkit
httpserver/server.go:206-221(the unsynchronized accessors in v2.8.0) - gitlab-org/labkit
httpserver/server.go:228-262(the writer inStartin v2.8.0) - gitlab-org/labkit!504 (merged) — upstream fix (merged 2026-05-26, in v2.10.2)
- gitlab-org/ops/artifact-registry!291 — Step 6 MR introducing the AR-side barrier workaround
- gitlab-org/ops/artifact-registry — issue #98 (sibling LabKit-v2 follow-up: dual-listener spec drift and AC-7 / AC-18 dispatch-model drift)