-
Notifications
You must be signed in to change notification settings - Fork 290
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
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
There was a problem hiding this 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).
Line 77 in 14f0965
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.
There was a problem hiding this 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 :)
app.go
Outdated
if code := app.run(app.Done()); code != 0 { | ||
code := app.run(app.Done()) | ||
if app.exitCode != 0 { | ||
code = app.exitCode |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
Done |
e2a945d
to
978250b
Compare
func (e exitCodeOption) apply(s *shutdowner) { | ||
s.app.exitCode = int(e) | ||
} |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
// WithExitCode allows the user to configure the exitCode upon application | ||
// shutdown. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
Signal os.Signal | ||
ExitCode int |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
This has been completed with #989. |
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(...))
app.Run()
, the exit code is read directly and returned.app.Start()
andapp.Stop()
, a new functionapp.DoneWithCode()
is presented that works similar toapp.Done()
. The following code illustrats its usage -Refs: GO-981, #763