🔧 chore(config): defense-in-depth for WatchAndApply test race (Q-038) #50

Merged
arcodange merged 1 commits from fix/config-test-main-quiet-zerolog into main 2026-05-05 09:45:15 +02:00
2 changed files with 35 additions and 4 deletions

View File

@@ -730,10 +730,15 @@ func (c *Config) WatchAndApply(ctx context.Context) {
// Stop the watcher on context cancel — we set a flag that the // Stop the watcher on context cancel — we set a flag that the
// OnConfigChange handler checks, avoiding the race with viper's // OnConfigChange handler checks, avoiding the race with viper's
// internal state that would occur if we called OnConfigChange again. // internal state that would occur if we called OnConfigChange again.
// We deliberately do NOT log here: viper's internal watcher goroutine //
// has no public Stop, so it can outlive ctx, and a zerolog call here // We deliberately do NOT log inside this goroutine: this goroutine
// would race with the next test's LoadConfig → SetupLogging → // outlives ctx (parent's defer cancel only fires when the test's
// zerolog.SetGlobalLevel under -race (observed 2026-05-05). // outer scope exits, not when t.Cleanup runs), so a log call here
// races with the next test's LoadConfig → SetupLogging →
// zerolog.SetGlobalLevel under -race (observed 2026-05-05, Q-038).
// The flag-set is the load-bearing operation; the missing log line
// is a small ops cost (operators learn the watcher stops on shutdown
// via the parent shutdown logs, not a dedicated message).
go func() { go func() {
<-ctx.Done() <-ctx.Done()
c.reloadMu.Lock() c.reloadMu.Lock()

26
pkg/config/main_test.go Normal file
View File

@@ -0,0 +1,26 @@
package config
import (
"os"
"testing"
"github.com/rs/zerolog"
)
// TestMain quiets the global zerolog level for the duration of the test
// suite. Rationale (Q-038, 2026-05-05): viper's internal watcher goroutine
// (started by viper.WatchConfig in WatchAndApply) has no public Stop and
// can outlive a test's context. Any log call from a leaked goroutine
// races with the next test's LoadConfig → SetupLogging →
// zerolog.SetGlobalLevel under `go test -race`. Disabling the logger here
// is the root-cause fix: the racing memory location is zerolog's gLevel
// global, and if no log call ever evaluates against it we sidestep the
// race entirely without changing production behavior.
//
// In production, log calls happen against an unchanging global level
// (SetupLogging runs once at startup), so the race condition does not
// occur there.
func TestMain(m *testing.M) {
zerolog.SetGlobalLevel(zerolog.Disabled)
os.Exit(m.Run())
}