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

Add fx.ShutdownError option #996

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 40 additions & 3 deletions shutdown.go
Expand Up @@ -23,6 +23,8 @@ package fx
import (
"context"
"time"

"go.uber.org/multierr"
)

// Shutdowner provides a method that can manually trigger the shutdown of the
Expand All @@ -34,19 +36,19 @@ type Shutdowner interface {
}

// ShutdownOption provides a way to configure properties of the shutdown
// process. Currently, no options have been implemented.
// process.
type ShutdownOption interface {
apply(*shutdowner)
}

type exitCodeOption int

var _ ShutdownOption = exitCodeOption(0)

func (code exitCodeOption) apply(s *shutdowner) {
s.exitCode = int(code)
}

var _ ShutdownOption = exitCodeOption(0)

// ExitCode is a [ShutdownOption] that may be passed to the Shutdown method of the
// [Shutdowner] interface.
// The given integer exit code will be broadcasted to any receiver waiting
Expand All @@ -71,6 +73,41 @@ func ShutdownTimeout(timeout time.Duration) ShutdownOption {
return shutdownTimeoutOption(timeout)
}

type shutdownErrorOption []error

func (errs shutdownErrorOption) apply(s *shutdowner) {
s.app.err = multierr.Append(s.app.err, multierr.Combine(errs...))
}

var _ ShutdownOption = shutdownErrorOption([]error{})

// ShutdownError registers any number of errors with the application shutdown.
// If more than one error is given, the errors are combined into a
// single error. Similar to invocations, errors are applied in order.
//
// You can use these errors, for example, to decide what to do after the app shutdown.
//
// customErr := errors.New("something went wrong")
// app := fx.New(
// ...
// fx.Provide(func(s fx.Shutdowner, a A) B {
// s.Shutdown(fx.ShutdownError(customErr))
// }),
// ...
// )
// err := app.Start(context.Background())
// if err != nil {
// panic(err)
// }
// defer app.Stop(context.Background())
//
// if err := app.Err(); errors.Is(err, customErr) {
// // custom logic here
// }
func ShutdownError(errs ...error) ShutdownOption {
return shutdownErrorOption(errs)
}

type shutdowner struct {
app *App
exitCode int
Expand Down
23 changes: 23 additions & 0 deletions shutdown_test.go
Expand Up @@ -22,6 +22,7 @@ package fx_test

import (
"context"
"errors"
"fmt"
"sync"
"testing"
Expand Down Expand Up @@ -128,6 +129,28 @@ func TestShutdown(t *testing.T) {

assert.NoError(t, s.Shutdown(fx.ExitCode(2), fx.ShutdownTimeout(time.Second)))
})

t.Run("with shutdown error", func(t *testing.T) {
t.Parallel()

var s fx.Shutdowner
app := fxtest.New(
t,
fx.Populate(&s),
)

done := app.Done()
wait := app.Wait()
defer app.RequireStart().RequireStop()

var expectedError = errors.New("shutdown error")

assert.NoError(t, s.Shutdown(fx.ShutdownError(expectedError)), "error in app shutdown")
assert.NotNil(t, <-done, "done channel did not receive signal")
assert.NotNil(t, <-wait, "wait channel did not receive signal")
assert.ErrorIs(t, app.Err(), expectedError,
"unexpected error, expected: %q, got: %q", expectedError, app.Err())
})
}

func TestDataRace(t *testing.T) {
Expand Down