Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support app shutdown with exit codes #808

Closed
wants to merge 10 commits into from
Closed

Conversation

manjari25
Copy link
Contributor

@manjari25 manjari25 commented Nov 10, 2021

We provide a way to shutdown an app via the Shutdowner interface but, we do not provide a way to specify/change the exit code. This commit adds support for shutting down apps with an exit code. Exit code can be controlled by providing a new Option to configure the Shutdowner. This can be done via shutdowner.Shutdown(WithExitCode(...))

  • For apps that use app.Run(), the exit code is read directly and returned.
  • For apps that are orchestrated with app.Start() and app.Stop(), a new function app.DoneWithCode() is presented that works similar to app.Done(). The following code illustrats its usage -
app := ...
app.Start(...)
done := app.DoneWithCode()
res := <-done
app.Stop(...)
res.ExitCode // captures the exit code

Refs: GO-981, #763

@codecov
Copy link

codecov bot commented Nov 10, 2021

Codecov Report

Merging #808 (5d714d6) into master (cc87b09) will decrease coverage by 0.09%.
The diff coverage is 93.33%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #808      +/-   ##
==========================================
- Coverage   99.25%   99.16%   -0.10%     
==========================================
  Files          24       24              
  Lines         941      953      +12     
==========================================
+ Hits          934      945      +11     
- Misses          6        7       +1     
  Partials        1        1              
Impacted Files Coverage Δ
shutdown.go 94.73% <75.00%> (-5.27%) ⬇️
app.go 99.67% <100.00%> (+<0.01%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cc87b09...5d714d6. Read the comment docs.

Copy link
Collaborator

@abhinav abhinav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE: testing:
check out the hidden WithExit option (only available in tests).

func WithExit(f func(int)) Option {

usage:
https:/uber-go/fx/blob/master/app_test.go#L842

Using that, you can capture the exit code that an app.Run would have exited with.

shutdown.go Show resolved Hide resolved
@manjari25 manjari25 marked this pull request as ready for review November 11, 2021 01:41
Copy link
Contributor

@sywhang sywhang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you modify the PR title/description so that it includes the public API's name (DoneWithCode) in it? It'll make it easier to locate it from commit history later :)

shutdown.go Outdated Show resolved Hide resolved
app.go Outdated Show resolved Hide resolved
app.go Outdated
if code := app.run(app.Done()); code != 0 {
code := app.run(app.Done())
if app.exitCode != 0 {
code = app.exitCode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kinda ugly. Rather than doing this, can you change exitCode's default value to be 1 and change run to return app.exitCode on error cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, will that not rewrite the exitCode set upon app shutdown?

@manjari25
Copy link
Contributor Author

Can you modify the PR title/description so that it includes the public API's name (DoneWithCode) in it? It'll make it easier to locate it from commit history later :)

Done

Comment on lines +44 to +46
func (e exitCodeOption) apply(s *shutdowner) {
s.app.exitCode = int(e)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I missed this before:
ShutdownOption is an option for a single Shutdown invocation. It doesn't need to store state on the application-scoped App object. It should store state either on the shutdowner, or better yet, on a shutdownOptions struct that the functional options turn into.

The standard functional options pattern we have in our style guide uses an internal options struct for the options.

For example,

type shutdownOptions struct {
  ExitCode int
}

type ShutdownOption interface{ apply(*shutdownOptions) }

func WithExitCode(int) ShutdownOption

I feel like the functional options should resolve the exit code in the s.Shutdown call, and send the appropriate value into the channel from there.

Then in Run, we'll use DoneWithCode and the returned exit code and all the state management for app.exitCode can be dropped.

}

func (e exitCodeOption) String() string {
return fmt.Sprintf("s.Shutdown(%v)", int(e))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be fx.WithExitCode.

Comment on lines +52 to +53
// WithExitCode allows the user to configure the exitCode upon application
// shutdown.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about,

WithExitCode specifies the exit code for an application shutdown. Applications using App.Run will exit the program with this exit code. Applications using App.DoneWithCode will return this exit code in the Shutdown struct.

Defaults to 0.

// DoneWithCode returns a channel of the Shutdown struct to block on after starting
// the application. This behaves exactly as Done() does with the addition of
// capturing the application's exit code, if one is set.
func (app *App) DoneWithCode() <-chan Shutdown {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we find an alternative name for this? DoneWithCode isn't enough because we're returning a whole Shutdown struct that can conceivably contain more information in the future.

I can't think of anything obvious yet. Maybe "Wait" or similar? We can discuss this.

@@ -804,6 +810,25 @@ func (app *App) Done() <-chan os.Signal {
return c
}

// Shutdown captures a signal and exit code.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutdown is a request to shut down the application.

Comment on lines +815 to +816
Signal os.Signal
ExitCode int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document these fields.

Signal is the signal that caused the shutdown.
For manual shutdowns with Shutdowner, it is TODO.

(Figure out the value.)

ExitCode specifies the code the program should exit with.
For programs terminated with SIGINT, this is zero.
For programs terminated with Shutdowner, callers may specify the exit code using the WithExitCode function.

@@ -674,8 +675,13 @@ func (app *App) run(done <-chan os.Signal) (exitCode int) {
startCtx, cancel := context.WithTimeout(context.Background(), app.StartTimeout())
defer cancel()

exitCode = 1
if app.exitCode != 0 {
exitCode = app.exitCode
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't give us what we want for normal use cases.
Per our documentation, Shutdowner should be invoked from a lifecycle hook.
That's run when we call App.Start.
By that point, we'll have already captured the exit code.

We need to use the new DoneWithCode API below after the Start, and use the exit code from that.

t,
fx.Populate(&s),
fx.WithExit(exit),
fx.Invoke(shutdown),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect usage per our documentation of Shutdowner:

// In practice this means Shutdowner.Shutdown should not be called from an
// fx.Invoke, but from a fx.Lifecycle.OnStart hook.

Previously, this just wouldn't work.
That has likely changed since #805 so we can update the documentation,
but all usages of Shutdowner up until this point are inside a lifecycle hook (on or after App.Start),
so we need to test for that too.

@sywhang
Copy link
Contributor

sywhang commented Dec 23, 2022

This has been completed with #989.

@sywhang sywhang closed this Dec 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants