Skip to content

Commit cc87b09

Browse files
manjari25abhinav
andauthoredNov 10, 2021
Support app shutdown before app.Done() is called (uber-go#805)
* Record app shutdown and offer the signal on app.Done() * Add data race test * Add lock for shutdown signal * Pull test into function and block go routines on ready channel * lint * Return channel early if shutdown signal received * Remove comment * Remove fmt qualifier for assert error message * Clarify comment * Assert no error on shutdown for TestDataRace * Apply suggestions from code review * go fmt Co-authored-by: Abhinav Gupta <abg@uber.com>
1 parent 5bb9132 commit cc87b09

File tree

3 files changed

+78
-6
lines changed

3 files changed

+78
-6
lines changed
 

‎app.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,9 @@ type App struct {
379379
errorHooks []ErrorHandler
380380
validate bool
381381
// Used to signal shutdowns.
382-
donesMu sync.RWMutex
383-
dones []chan os.Signal
382+
donesMu sync.Mutex // guards dones and shutdownSig
383+
dones []chan os.Signal
384+
shutdownSig os.Signal
384385

385386
osExit func(code int) // os.Exit override; used for testing only
386387
}
@@ -787,11 +788,19 @@ func (app *App) Stop(ctx context.Context) (err error) {
787788
// using the Shutdown functionality (see the Shutdowner documentation for details).
788789
func (app *App) Done() <-chan os.Signal {
789790
c := make(chan os.Signal, 1)
790-
signal.Notify(c, _sigINT, _sigTERM)
791791

792792
app.donesMu.Lock()
793+
defer app.donesMu.Unlock()
794+
// If shutdown signal has been received already
795+
// send it and return. If not, wait for user to send a termination
796+
// signal.
797+
if app.shutdownSig != nil {
798+
c <- app.shutdownSig
799+
return c
800+
}
801+
802+
signal.Notify(c, _sigINT, _sigTERM)
793803
app.dones = append(app.dones, c)
794-
app.donesMu.Unlock()
795804
return c
796805
}
797806

‎shutdown.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ func (app *App) shutdowner() Shutdowner {
5757
}
5858

5959
func (app *App) broadcastSignal(signal os.Signal) error {
60-
app.donesMu.RLock()
61-
defer app.donesMu.RUnlock()
60+
app.donesMu.Lock()
61+
defer app.donesMu.Unlock()
62+
63+
app.shutdownSig = signal
6264

6365
var unsent int
6466
for _, done := range app.dones {

‎shutdown_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
package fx_test
2222

2323
import (
24+
"context"
25+
"sync"
2426
"testing"
2527

2628
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
2730
"go.uber.org/fx"
2831
"go.uber.org/fx/fxtest"
2932
)
@@ -65,4 +68,62 @@ func TestShutdown(t *testing.T) {
6568
"unexpected error returned when shutdown is called with a blocked channel")
6669
assert.NotNil(t, <-done, "done channel did not receive signal")
6770
})
71+
72+
t.Run("shutdown app before calling Done()", func(t *testing.T) {
73+
t.Parallel()
74+
75+
var s fx.Shutdowner
76+
app := fxtest.New(
77+
t,
78+
fx.Populate(&s),
79+
)
80+
81+
require.NoError(t, app.Start(context.Background()), "error starting app")
82+
assert.NoError(t, s.Shutdown(), "error in app shutdown")
83+
done1, done2 := app.Done(), app.Done()
84+
defer app.Stop(context.Background())
85+
// Receiving on done1 and done2 will deadlock in the event that app.Done()
86+
// doesn't work as expected.
87+
assert.NotNil(t, <-done1, "done channel 1 did not receive signal")
88+
assert.NotNil(t, <-done2, "done channel 2 did not receive signal")
89+
})
90+
}
91+
92+
func TestDataRace(t *testing.T) {
93+
t.Parallel()
94+
95+
var s fx.Shutdowner
96+
app := fxtest.New(
97+
t,
98+
fx.Populate(&s),
99+
)
100+
require.NoError(t, app.Start(context.Background()), "error starting app")
101+
102+
const N = 50
103+
ready := make(chan struct{}) // used to orchestrate goroutines for Done() and ShutdownOption()
104+
var wg sync.WaitGroup // tracks and waits for all goroutines
105+
106+
// Spawn N goroutines, each of which call app.Done() and assert
107+
// the signal received.
108+
wg.Add(N)
109+
for i := 0; i < N; i++ {
110+
i := i
111+
go func() {
112+
defer wg.Done()
113+
<-ready
114+
done := app.Done()
115+
assert.NotNil(t, <-done, "done channel %v did not receive signal", i)
116+
}()
117+
}
118+
119+
// call Shutdown()
120+
wg.Add(1)
121+
go func() {
122+
<-ready
123+
defer wg.Done()
124+
assert.NoError(t, s.Shutdown(), "error in app shutdown")
125+
}()
126+
127+
close(ready)
128+
wg.Wait()
68129
}

0 commit comments

Comments
 (0)
Please sign in to comment.