Skip to content

Commit ce5172b

Browse files
committed
add more race condition prevention tests
1 parent d9620cc commit ce5172b

File tree

2 files changed

+57
-15
lines changed

2 files changed

+57
-15
lines changed

runnables/httpcluster/runner_test.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -497,14 +497,12 @@ func TestRunnerConcurrency(t *testing.T) {
497497
// Send multiple configs concurrently
498498
const numConfigs = 10
499499
var wg sync.WaitGroup
500-
wg.Add(numConfigs)
501500

502501
testCtx, testCancel := context.WithCancel(t.Context())
503502
defer testCancel()
504503

505-
for i := 0; i < numConfigs; i++ {
506-
go func(i int) {
507-
defer wg.Done()
504+
for range numConfigs {
505+
wg.Go(func() {
508506
// Each concurrent update uses a different port
509507
addr := networking.GetRandomListeningPort(t)
510508
configs := map[string]*httpserver.Config{
@@ -519,7 +517,7 @@ func TestRunnerConcurrency(t *testing.T) {
519517
case <-ctx.Done():
520518
// Timeout is ok for buffered channel
521519
}
522-
}(i)
520+
})
523521
}
524522

525523
wg.Wait()
@@ -553,11 +551,9 @@ func TestRunnerConcurrency(t *testing.T) {
553551
// Read state concurrently
554552
const numReaders = 50
555553
var wg sync.WaitGroup
556-
wg.Add(numReaders)
557554

558-
for i := 0; i < numReaders; i++ {
559-
go func() {
560-
defer wg.Done()
555+
for range numReaders {
556+
wg.Go(func() {
561557
state := runner.GetState()
562558
assert.NotEmpty(t, state)
563559

@@ -566,7 +562,7 @@ func TestRunnerConcurrency(t *testing.T) {
566562

567563
str := runner.String()
568564
assert.Contains(t, str, "HTTPCluster")
569-
}()
565+
})
570566
}
571567

572568
wg.Wait()
@@ -593,13 +589,11 @@ func TestRunnerConcurrency(t *testing.T) {
593589
// Call Stop concurrently
594590
const numStops = 10
595591
var wg sync.WaitGroup
596-
wg.Add(numStops)
597592

598-
for i := 0; i < numStops; i++ {
599-
go func() {
600-
defer wg.Done()
593+
for range numStops {
594+
wg.Go(func() {
601595
runner.Stop()
602-
}()
596+
})
603597
}
604598

605599
wg.Wait()

runnables/httpserver/runner_race_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"sync"
8+
"sync/atomic"
89
"testing"
910
"time"
1011

@@ -159,3 +160,50 @@ func TestRunnerRaceConditions(t *testing.T) {
159160

160161
<-errChan
161162
}
163+
164+
// TestGetConfig_ConcurrentAccess attempts to prove a race condition in getConfig()
165+
// when multiple goroutines call it concurrently with nil config.
166+
// Since NewRunner eagerly loads config during construction, all concurrent
167+
// getConfig() calls find non-nil config and never hit the callback path.
168+
func TestGetConfig_ConcurrentAccess(t *testing.T) {
169+
t.Parallel()
170+
171+
var callbackCount atomic.Int64
172+
173+
handler := func(w http.ResponseWriter, r *http.Request) {
174+
w.WriteHeader(http.StatusOK)
175+
}
176+
route, err := NewRouteFromHandlerFunc("test", "/test", handler)
177+
require.NoError(t, err)
178+
179+
port := fmt.Sprintf(":%d", networking.GetRandomPort(t))
180+
181+
cfgCallback := func() (*Config, error) {
182+
callbackCount.Add(1)
183+
return NewConfig(port, Routes{*route}, WithDrainTimeout(1*time.Second))
184+
}
185+
186+
runner, err := NewRunner(WithConfigCallback(cfgCallback))
187+
require.NoError(t, err)
188+
189+
// NewRunner calls getConfig() once during construction
190+
assert.Equal(t, int64(1), callbackCount.Load())
191+
192+
const goroutines = 10
193+
configs := make([]*Config, goroutines)
194+
var wg sync.WaitGroup
195+
for i := range goroutines {
196+
wg.Go(func() {
197+
configs[i] = runner.getConfig()
198+
})
199+
}
200+
wg.Wait()
201+
202+
// Callback should still only have been called once — config was already loaded by NewRunner
203+
assert.Equal(t, int64(1), callbackCount.Load())
204+
205+
// All goroutines got the same config
206+
for i := range configs {
207+
assert.Same(t, configs[0], configs[i])
208+
}
209+
}

0 commit comments

Comments
 (0)