Skip to content

Commit ef9f22e

Browse files
committed
fix: eliminate cfg.Logger data race in server()'s reload/shutdown path
Loader.New() aliases the *config.Server passed to it, so the watcher goroutine's cfg.Logger reassignment on each config reload directly mutates the same struct instance server()'s top-level select loop reads from when logging shutdown/hook-error messages. The watcher writes under cfgMu, but server() read cfg.Logger with no synchronization at all, producing a genuine data race (confirmed via the actual CI job log: write at configloader.go:91 vs. read at server.go:112, and reproduced locally under linux/-race). Fix: the loader publishes each new logger over a small "latest value" channel as reloads happen. server() keeps its own local logger reference and swaps it only upon receiving from that channel, so the update is synchronized via channel send/receive instead of a shared, unguarded field. Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
1 parent 8782e63 commit ef9f22e

3 files changed

Lines changed: 39 additions & 9 deletions

File tree

internal/cmd/server.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ func server(ctx context.Context, stdout, stderr io.Writer, cfgPath string, hook
8888
return err
8989
}
9090

91+
// cfg.Logger is mutated concurrently on reload, so keep our own reference
92+
// and update it only via l.LoggerUpdates() below, never read cfg.Logger again.
93+
logger := cfg.Logger
94+
9195
l := loader.New(cfgPath, cfg, hook)
9296
if err := l.Start(ctx, stdout); err != nil {
9397
return err
@@ -99,17 +103,20 @@ func server(ctx context.Context, stdout, stderr io.Writer, cfgPath string, hook
99103

100104
for {
101105
select {
106+
// Pick up the latest logger whenever a config reload replaces it.
107+
case newLogger := <-l.LoggerUpdates():
108+
logger = newLogger
102109
// Exit if the config loader fails to start the runners.
103110
// Continuing with failed runners would cause EG to function incorrectly.
104111
case err := <-l.Errors():
105-
cfg.Logger.Error(err, "failed to start runners")
112+
logger.Error(err, "failed to start runners")
106113
// Wait for runners to finish before shutting down.
107114
// This is to make sure no orphaned runner process is left running in standalone mode.
108115
l.Wait()
109116
return err
110117
// Wait for the context to be done, which usually happens the process receives a SIGTERM or SIGINT.
111118
case <-ctx.Done():
112-
cfg.Logger.Info("shutting down")
119+
logger.Info("shutting down")
113120
// Wait for runners to finish before shutting down.
114121
// This is to make sure no orphaned runner process is left running in standalone mode.
115122
l.Wait()

internal/envoygateway/config/loader/configloader.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,21 @@ type Loader struct {
2929
hook HookFunc
3030
hookErr chan error
3131

32+
// loggerUpdates publishes the logger on each config reload; see LoggerUpdates.
33+
loggerUpdates chan logging.Logger
34+
3235
w filewatcher.FileWatcher
3336
}
3437

3538
func New(cfgPath string, cfg *config.Server, f HookFunc) *Loader {
3639
return &Loader{
37-
cfgPath: cfgPath,
38-
cfg: cfg,
39-
logger: cfg.Logger.WithName("config-loader"),
40-
hook: f,
41-
hookErr: make(chan error, 1),
42-
w: filewatcher.NewWatcher(),
40+
cfgPath: cfgPath,
41+
cfg: cfg,
42+
logger: cfg.Logger.WithName("config-loader"),
43+
hook: f,
44+
hookErr: make(chan error, 1),
45+
loggerUpdates: make(chan logging.Logger, 1),
46+
w: filewatcher.NewWatcher(),
4347
}
4448
}
4549

@@ -85,11 +89,13 @@ func (r *Loader) Start(ctx context.Context, logOut io.Writer) error {
8589
continue
8690
}
8791

92+
newLogger := logging.NewLogger(logOut, eg.Logging)
8893
r.cfgMu.Lock()
8994
r.cfg.EnvoyGateway = eg
9095
// update cfg logger
91-
r.cfg.Logger = logging.NewLogger(logOut, eg.Logging)
96+
r.cfg.Logger = newLogger
9297
r.cfgMu.Unlock()
98+
r.publishLogger(newLogger)
9399

94100
// cancel last
95101
if r.cancel != nil {
@@ -142,6 +148,22 @@ func (r *Loader) Errors() <-chan error {
142148
return r.hookErr
143149
}
144150

151+
// LoggerUpdates returns a channel that receives the latest logger on each config reload.
152+
func (r *Loader) LoggerUpdates() <-chan logging.Logger {
153+
return r.loggerUpdates
154+
}
155+
156+
func (r *Loader) publishLogger(l logging.Logger) {
157+
select {
158+
case <-r.loggerUpdates: // drain a stale, unconsumed value, if any
159+
default:
160+
}
161+
select {
162+
case r.loggerUpdates <- l:
163+
default:
164+
}
165+
}
166+
145167
// Wait returns when success to acquire mutex, which means no hook is running.
146168
func (r *Loader) Wait() {
147169
r.hookMutex.Lock()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a data race between config-reload and the standalone server's shutdown/error logging by having the config loader publish logger updates over a channel instead of a shared, unguarded field.

0 commit comments

Comments
 (0)