@@ -4,15 +4,17 @@ It provides a way to register functions that will be called when the application
44
55The application is considered to be running after calling Exitplan.Run() and before calling Exitplan.Exit().
66Use Exitplan.Started() to receive a signal when the application enters the running phase.
7+ Use Exitplan.Stopping() to receive a signal when the application enters the teardown phase.
78Use Exitplan.Context() to get a context bound to the application lifetime.
9+ Use Exitplan.Completed() to receive a signal when teardown is completed.
810
911The application is considered to be tearing down after calling Exitplan.Exit().
1012You can use Exitplan.TeardownContext() to get a context that can be used to control the teardown phase.
1113It is canceled when the teardown timeout is reached.
1214
1315Ordering of shutdown callbacks
1416Exitplan executes registered exit callbacks in LIFO order (last registered, first executed).
15- Async only offloads the execution to a goroutine, but Exitplan still waits for all callbacks up to the
17+ Async only offloads the execution to a goroutine, but the Exitplan still waits for all callbacks up to the
1618teardown timeout.
1719*/
1820package exitplan
@@ -43,8 +45,7 @@ type Exitplan struct {
4345 runningCtx context.Context
4446 runningCancel context.CancelCauseFunc
4547
46- startingCtx context.Context
47- startingCancel context.CancelFunc
48+ started chan struct {}
4849 startingTimeout time.Duration
4950
5051 teardownCtx context.Context
@@ -53,6 +54,10 @@ type Exitplan struct {
5354
5455 callbacks []* callback
5556
57+ panicValue any
58+ panicOnce sync.Once
59+ ranViaRun bool
60+
5661 errorHandler func (error )
5762 callbacksMu * sync.Mutex
5863}
@@ -64,6 +69,7 @@ func New(opts ...opt) *Exitplan {
6469 teardownCtx , teardownCancel := context .WithCancel (context .Background ())
6570 l := & Exitplan {
6671 die : make (chan struct {}),
72+ started : make (chan struct {}),
6773 callbacksMu : & sync.Mutex {},
6874 callbacks : make ([]* callback , 0 ),
6975
@@ -85,26 +91,57 @@ func New(opts ...opt) *Exitplan {
8591func (l * Exitplan ) start () {
8692 l .phase .Store (int32 (phaseStarting ))
8793
88- ctx , cancel := context .WithCancel (context .Background ())
8994 if l .startingTimeout > 0 {
90- ctx , cancel = context .WithTimeout (ctx , l .startingTimeout )
95+ ctx , cancel : = context .WithTimeout (context . Background () , l .startingTimeout )
9196 go func () {
97+ defer cancel ()
9298 <- ctx .Done ()
9399
94- if l .phase .Load () == int32 (phaseStarting ) {
100+ if l .phase .CompareAndSwap ( int32 ( phaseStarting ), int32 (phaseTeardown ) ) {
95101 l .runningCancel (ErrStartupTimeout )
96102 close (l .die )
97103 }
98104 }()
99105 }
100106
101- l .startingCtx = ctx
102- l .startingCancel = cancel
107+ go func () {
108+ <- l .die
109+
110+ go func () {
111+ if l .teardownTimeout > 0 {
112+ <- time .After (l .teardownTimeout )
113+ l .teardownCancel ()
114+ }
115+ }()
116+
117+ defer l .teardownCancel ()
118+ defer func () {
119+ if r := recover (); r != nil {
120+ l .capturePanic (r )
121+ }
122+ }()
123+
124+ l .exit ()
125+ }()
103126}
104127
105- // Started returns a channel that is closed after Run is called.
128+ // Started returns a channel that is closed after Run is called and the application enters the running phase.
129+ // It will never fire if Exit is called before Run, or if the startup timeout expires.
106130func (l * Exitplan ) Started () <- chan struct {} {
107- return l .startingCtx .Done ()
131+ return l .started
132+ }
133+
134+ // Completed returns a channel closed after either all callback or teardown context is completed.
135+ // This signal means that now the application can exit.
136+ func (l * Exitplan ) Completed () <- chan struct {} {
137+ return l .teardownCtx .Done ()
138+ }
139+
140+ // Stopping returns a channel closed when the teardown phase starts.
141+ // This is equivalent to calling Exitplan.Context().Done().
142+ // It's fired before the application enters the teardown phase by calling Exit.
143+ func (l * Exitplan ) Stopping () <- chan struct {} {
144+ return l .runningCtx .Done ()
108145}
109146
110147// Context returns a main context. It will be canceled when the application is about to exit.
@@ -121,7 +158,7 @@ func (l *Exitplan) TeardownContext() context.Context {
121158}
122159
123160// OnExit registers a callback that will be called when the application is about to exit.
124- // Has no effect after calling Exitplan.Run().
161+ // Has no effect after calling Exitplan.Run() or Exitplan.Exit() .
125162// Use exitplan.Async option to execute the callback in a separate goroutine.
126163// exitplan.PanicOnError has no effect on this function.
127164// See also: OnExitWithError, OnExitWithContext, OnExitWithContextError.
@@ -135,7 +172,7 @@ func (l *Exitplan) OnExit(callback func(), exitOpts ...exitCallbackOpt) {
135172}
136173
137174// OnExitWithError registers a callback that will be called when the application is about to exit.
138- // Has no effect after calling Exitplan.Run().
175+ // Has no effect after calling Exitplan.Run() or Exitplan.Exit() .
139176// The callback can return an error that will be passed to the error handler.
140177// Use exitplan.Async option to execute the callback in a separate goroutine.
141178// Use exitplan.PanicOnError to panic with the error returned by the callback.
@@ -146,7 +183,7 @@ func (l *Exitplan) OnExitWithError(callback func() error, exitOpts ...exitCallba
146183}
147184
148185// OnExitWithContext registers a callback that will be called when the application is about to exit.
149- // Has no effect after calling Exitplan.Run().
186+ // Has no effect after calling Exitplan.Run() or Exitplan.Exit() .
150187// The callback will receive a context that will be canceled after the teardown timeout.
151188// Use exitplan.Async option to execute the callback in a separate goroutine.
152189// exitplan.PanicOnError has no effect on this function.
@@ -158,7 +195,7 @@ func (l *Exitplan) OnExitWithContext(callback func(context.Context), exitOpts ..
158195}
159196
160197// OnExitWithContextError registers a callback that will be called when the application is about to exit.
161- // Has no effect after calling Exitplan.Run().
198+ // Has no effect after calling Exitplan.Run() or Exitplan.Exit() .
162199// The callback will receive a context that will be canceled after the teardown timeout.
163200// The callback can return an error that will be passed to the error handler.
164201// Use exitplan.Async option to execute the callback in a separate goroutine.
@@ -168,10 +205,6 @@ func (l *Exitplan) OnExitWithContextError(callback func(context.Context) error,
168205}
169206
170207func (l * Exitplan ) addCallback (cb func (context.Context ) error , exitOpts ... exitCallbackOpt ) {
171- if l .phase .Load () != int32 (phaseStarting ) {
172- return
173- }
174-
175208 c := & callback {
176209 name : callerLocation (3 ),
177210 fn : cb ,
@@ -183,63 +216,84 @@ func (l *Exitplan) addCallback(cb func(context.Context) error, exitOpts ...exitC
183216
184217 l .callbacksMu .Lock ()
185218 defer l .callbacksMu .Unlock ()
219+
220+ if l .phase .Load () != int32 (phaseStarting ) {
221+ return
222+ }
223+
186224 l .callbacks = append (l .callbacks , c )
187225}
188226
189- // Exit stops the application. It will cause the application to exit with the specified reason.
227+ // Exit stops the application and waits for all registered callbacks to complete.
228+ // It returns the provided reason, enabling the pattern: return lc.Exit(err).
190229// If the reason is nil, ErrGracefulShutdown will be used.
191- // Multiple calls to Exit are safe, but only the first one will have an effect.
192- func (l * Exitplan ) Exit (reason error ) {
230+ // Exit can be called before or after Run.
231+ // Multiple calls to Exit are safe; all will block until teardown completes.
232+ func (l * Exitplan ) Exit (reason error ) error {
193233 if reason == nil {
194234 reason = ErrGracefulShutdown
195235 }
196236
197- if ! l .phase .CompareAndSwap (int32 (phaseRunning ), int32 (phaseTeardown )) {
198- return
237+ if l .phase .CompareAndSwap (int32 (phaseRunning ), int32 (phaseTeardown )) ||
238+ l .phase .CompareAndSwap (int32 (phaseStarting ), int32 (phaseTeardown )) {
239+ l .runningCancel (reason )
240+ close (l .die )
199241 }
200242
201- l .runningCancel (reason )
202- close (l .die )
243+ <- l .teardownCtx .Done ()
244+
245+ if ! l .ranViaRun && l .panicValue != nil {
246+ panic (l .panicValue )
247+ }
248+
249+ return context .Cause (l .runningCtx )
203250}
204251
205252// Run starts the application. It will block until the application is stopped by calling Exit.
206253// It will also block until all the registered callbacks are executed.
207254// If the teardown timeout is set, it will be used to cancel the context passed to the callbacks.
255+ // If Exit was called before Run, Run will wait for teardown to complete and return the exit cause.
208256// Returns the error that caused the application to stop.
209257func (l * Exitplan ) Run () (exitCause error ) {
210- if ! l .phase .CompareAndSwap (int32 (phaseStarting ), int32 (phaseRunning )) {
211- panic ("Exitplan.Run() called after Exitplan.Exit()" )
212- }
258+ l .ranViaRun = true
213259
214- l .startingCancel ()
215-
216- <- l .die
260+ // If Exit() was already called, skip the starting→running transition.
261+ if l .phase .CompareAndSwap (int32 (phaseStarting ), int32 (phaseRunning )) {
262+ close (l .started )
263+ }
217264
218- go func () {
219- if l .teardownTimeout > 0 {
220- <- time .After (l .teardownTimeout )
221- l .teardownCancel ()
222- }
223- }()
265+ <- l .teardownCtx .Done ()
224266
225- l .exit ()
267+ if l .panicValue != nil {
268+ panic (l .panicValue )
269+ }
226270
227271 return context .Cause (l .runningCtx )
228272}
229273
230274func (l * Exitplan ) exit () {
231275 ctx := l .TeardownContext ()
232276
277+ l .callbacksMu .Lock ()
278+ callbacks := make ([]* callback , len (l .callbacks ))
279+ copy (callbacks , l .callbacks )
280+ l .callbacksMu .Unlock ()
281+
233282 wg := sync.WaitGroup {}
234283
235- for _ , cb := range l . callbacks {
284+ for _ , cb := range callbacks {
236285 if cb .executeBehaviour != executeAsync {
237286 continue
238287 }
239288
240289 wg .Add (1 )
241290 go func (cb * callback ) {
242291 defer wg .Done ()
292+ defer func () {
293+ if r := recover (); r != nil {
294+ l .capturePanic (r )
295+ }
296+ }()
243297
244298 execCtx := ctx
245299 if cb .timeout > 0 {
@@ -257,8 +311,8 @@ func (l *Exitplan) exit() {
257311 }(cb )
258312 }
259313
260- for i := len (l . callbacks ) - 1 ; i >= 0 ; i -- {
261- cb := l . callbacks [i ]
314+ for i := len (callbacks ) - 1 ; i >= 0 ; i -- {
315+ cb := callbacks [i ]
262316 if cb .executeBehaviour != executeSync {
263317 continue
264318 }
@@ -303,6 +357,12 @@ func (l *Exitplan) handleExitError(errBehaviour exitBehaviour, err error) {
303357 }
304358}
305359
360+ func (l * Exitplan ) capturePanic (r any ) {
361+ l .panicOnce .Do (func () {
362+ l .panicValue = r
363+ })
364+ }
365+
306366func callbackWithContext (ctx context.Context , callback func () error ) error {
307367 errChan := make (chan error )
308368 go func () {
0 commit comments