From b7b297c57644126c9a06ba8cbf6cf51dfefebdfb Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Wed, 6 May 2026 15:02:59 +0100 Subject: [PATCH] test: make MockStatusUpdater thread-safe for concurrent callbacks The mock recorded calls into unprotected slices while being invoked concurrently from the test goroutine and from the health-checker callback registered by Manager.startWorker (and the watchdog hung callback). go test -race -count=20 reliably tripped the race detector on TestStartWorker_HealthCallback_StatusChange. Wrap the slice writes in a sync.Mutex. Read-side code in tests reads only after manager.Stop() drains the background goroutines (Stop's wg.Wait establishes a happens-before edge), so direct reads remain race-free. --- internal/forward/manager_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/forward/manager_test.go b/internal/forward/manager_test.go index b386c5d..5d76639 100644 --- a/internal/forward/manager_test.go +++ b/internal/forward/manager_test.go @@ -2,6 +2,7 @@ package forward import ( "fmt" + "sync" "testing" "time" @@ -172,12 +173,18 @@ func TestManager_getResourceForPort(t *testing.T) { assert.Equal(t, "unknown", resource) } -// MockStatusUpdater is a mock implementation of StatusUpdater +// MockStatusUpdater is a mock implementation of StatusUpdater. Methods are +// invoked concurrently from the test goroutine and from the health-checker / +// watchdog goroutines registered by Manager.startWorker, so the recorded +// slices are guarded by mu. Tests inspect the slices only after Manager.Stop +// has drained the background goroutines (Stop's wg.Wait establishes a +// happens-before edge) so the read side does not need to hold mu. type MockStatusUpdater struct { updates []StatusUpdate adds []ForwardAdd removes []string errorSets []ErrorSet + mu sync.Mutex } type StatusUpdate struct { @@ -196,18 +203,26 @@ type ErrorSet struct { } func (m *MockStatusUpdater) UpdateStatus(id string, status string) { + m.mu.Lock() + defer m.mu.Unlock() m.updates = append(m.updates, StatusUpdate{ID: id, Status: status}) } func (m *MockStatusUpdater) AddForward(id string, fwd *config.Forward) { + m.mu.Lock() + defer m.mu.Unlock() m.adds = append(m.adds, ForwardAdd{ID: id, Fwd: fwd}) } func (m *MockStatusUpdater) Remove(id string) { + m.mu.Lock() + defer m.mu.Unlock() m.removes = append(m.removes, id) } func (m *MockStatusUpdater) SetError(id, msg string) { + m.mu.Lock() + defer m.mu.Unlock() m.errorSets = append(m.errorSets, ErrorSet{ID: id, Msg: msg}) }