Skip to content

Commit 848742a

Browse files
committed
fix goleak and data races around stop channels for signal broadcast by using mutex
1 parent b4e5c2a commit 848742a

File tree

2 files changed

+40
-7
lines changed

2 files changed

+40
-7
lines changed

app.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ type App struct {
275275
err error
276276
clock fxclock.Clock
277277
lifecycle *lifecycleWrapper
278-
stopch chan struct{} // closed when Stop is called
278+
279+
stopch chan struct{} // closed when Stop is called
280+
stopChLock sync.RWMutex // mutex for init and closing of stopch
279281

280282
container *dig.Container
281283
root *module
@@ -400,7 +402,6 @@ func New(opts ...Option) *App {
400402
clock: fxclock.System,
401403
startTimeout: DefaultTimeout,
402404
stopTimeout: DefaultTimeout,
403-
stopch: make(chan struct{}),
404405
}
405406

406407
app.root = &module{
@@ -563,6 +564,7 @@ func (app *App) run(done <-chan ShutdownSignal) (exitCode int) {
563564
defer cancel()
564565

565566
if err := app.Start(startCtx); err != nil {
567+
app.closeStopChannel()
566568
return 1
567569
}
568570

@@ -628,6 +630,8 @@ func (app *App) Start(ctx context.Context) (err error) {
628630
return app.err
629631
}
630632

633+
app.initStopChannel()
634+
631635
return withTimeout(ctx, &withTimeoutParams{
632636
hook: _onStartHook,
633637
callback: app.start,
@@ -653,6 +657,30 @@ func (app *App) start(ctx context.Context) error {
653657
return nil
654658
}
655659

660+
func (app *App) initStopChannel() {
661+
app.stopChLock.Lock()
662+
defer app.stopChLock.Unlock()
663+
if app.stopch == nil {
664+
app.stopch = make(chan struct{})
665+
}
666+
}
667+
668+
func (app *App) stopChannel() chan struct{} {
669+
app.stopChLock.RLock()
670+
defer app.stopChLock.RUnlock()
671+
ch := app.stopch
672+
return ch
673+
}
674+
675+
func (app *App) closeStopChannel() {
676+
app.stopChLock.Lock()
677+
defer app.stopChLock.Unlock()
678+
if app.stopch != nil {
679+
close(app.stopch)
680+
app.stopch = nil
681+
}
682+
}
683+
656684
// Stop gracefully stops the application. It executes any registered OnStop
657685
// hooks in reverse order, so that each constructor's stop hooks are called
658686
// before its dependencies' stop hooks.
@@ -666,7 +694,7 @@ func (app *App) Stop(ctx context.Context) (err error) {
666694
// Protect the Stop hooks from being called multiple times.
667695
app.runStop.Do(func() {
668696
app.log().LogEvent(&fxevent.Stopped{Err: err})
669-
close(app.stopch)
697+
app.closeStopChannel()
670698
})
671699
}()
672700

@@ -724,10 +752,14 @@ func (app *App) appendSignalReceiver(r signalReceiver) {
724752
sigch := make(chan os.Signal, 1)
725753
signal.Notify(sigch, os.Interrupt, _sigINT, _sigTERM)
726754
go func() {
727-
select {
728-
case sig := <-sigch:
729-
app.broadcastSignal(sig, 1)
730-
case <-app.stopch:
755+
// if the stop channel is nil; that means that the app was never started
756+
// thus, do not broadcast any signals
757+
if stopch := app.stopChannel(); stopch != nil {
758+
select {
759+
case sig := <-sigch:
760+
app.broadcastSignal(sig, 1)
761+
case <-stopch:
762+
}
731763
}
732764
}()
733765
})

shutdown_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func TestDataRace(t *testing.T) {
171171
fx.Populate(&s),
172172
)
173173
require.NoError(t, app.Start(context.Background()), "error starting app")
174+
defer require.NoError(t, app.Stop(context.Background()), "error stopping app")
174175

175176
const N = 50
176177
ready := make(chan struct{}) // used to orchestrate goroutines for Done() and ShutdownOption()

0 commit comments

Comments
 (0)