-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Andrei Smirnov
committed
May 12, 2023
1 parent
444145b
commit 91aba22
Showing
3 changed files
with
166 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package boot | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
"time" | ||
|
||
"go.uber.org/multierr" | ||
) | ||
|
||
// Application represents an application that supports graceful boot/shutdown. | ||
type Applicaiton interface { | ||
// Start starts the application. This is a blocking, not thread-safe call. | ||
// This method handles SIGTERM/SIGINT signals and automatically shutting down the app. | ||
// The provided user context can be used to signal application to stop gracefully. | ||
Run(ctx context.Context) error | ||
} | ||
|
||
type application struct { | ||
service Service | ||
shutdownTimeout time.Duration | ||
} | ||
|
||
var _ Applicaiton = (*application)(nil) | ||
|
||
// NewApplicationForService creates a new Application from the given (uber) Service, | ||
// that is usually constructed with Sequentially or Simultaneously (or combination). | ||
// A commonly recommended shutdownTimeout range is 5-15 seconds. | ||
func NewApplicationForService(service Service, shutdownTimeout time.Duration) Applicaiton { | ||
return &application{ | ||
service: service, | ||
shutdownTimeout: shutdownTimeout, | ||
} | ||
} | ||
|
||
func (a application) Run(ctx context.Context) error { | ||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) | ||
defer stop() | ||
|
||
startErr := a.service.Start(ctx) | ||
if startErr == nil { | ||
<-ctx.Done() | ||
} | ||
stop() | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), a.shutdownTimeout) | ||
defer cancel() | ||
|
||
return multierr.Combine(startErr, a.service.Stop(ctx)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package boot_test | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"syscall" | ||
"testing" | ||
"time" | ||
|
||
"github.com/pinebit/go-boot/boot" | ||
"github.com/pinebit/go-boot/boot/mocks" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
func TestApplication_Run(t *testing.T) { | ||
t.Parallel() | ||
|
||
ctx := testContext(t) | ||
s1 := mocks.NewService(t) | ||
s2 := mocks.NewService(t) | ||
s3 := mocks.NewService(t) | ||
s1.On("Start", mock.Anything).Run(func(args mock.Arguments) { | ||
s2.On("Start", mock.Anything).Run(func(args mock.Arguments) { | ||
s3.On("Start", mock.Anything).Return(nil) | ||
}).Return(nil) | ||
}).Return(nil) | ||
s3.On("Stop", mock.Anything).Run(func(args mock.Arguments) { | ||
s2.On("Stop", mock.Anything).Run(func(args mock.Arguments) { | ||
s1.On("Stop", mock.Anything).Return(nil) | ||
}).Return(nil) | ||
}).Return(nil) | ||
services := boot.Sequentially(s1, s2, s3) | ||
app := boot.NewApplicationForService(services, 5*time.Second) | ||
|
||
t.Run("shutting down due to SIGINT", func(t *testing.T) { | ||
go func() { | ||
<-time.After(100 * time.Millisecond) | ||
syscall.Kill(syscall.Getpid(), syscall.SIGINT) | ||
}() | ||
err := app.Run(ctx) | ||
|
||
assert.NoError(t, err) | ||
}) | ||
|
||
t.Run("shutting down due to SIGTERM", func(t *testing.T) { | ||
go func() { | ||
<-time.After(100 * time.Millisecond) | ||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM) | ||
}() | ||
err := app.Run(ctx) | ||
|
||
assert.NoError(t, err) | ||
}) | ||
|
||
t.Run("shutting down due to user context", func(t *testing.T) { | ||
uctx, cancel := context.WithCancel(ctx) | ||
go func() { | ||
<-time.After(100 * time.Millisecond) | ||
cancel() | ||
}() | ||
err := app.Run(uctx) | ||
|
||
assert.NoError(t, err) | ||
}) | ||
} | ||
|
||
func TestApplication_StartError(t *testing.T) { | ||
t.Parallel() | ||
|
||
startErr := errors.New("start") | ||
ctx := testContext(t) | ||
s1 := mocks.NewService(t) | ||
s1.On("Start", mock.Anything).Return(startErr) | ||
s1.On("Stop", mock.Anything).Return(nil) | ||
app := boot.NewApplicationForService(s1, 5*time.Second) | ||
|
||
err := app.Run(ctx) | ||
|
||
assert.ErrorIs(t, err, startErr) | ||
} | ||
|
||
func TestApplication_StopError(t *testing.T) { | ||
t.Parallel() | ||
|
||
stopErr := errors.New("stop") | ||
ctx, cancel := context.WithCancel(testContext(t)) | ||
s1 := mocks.NewService(t) | ||
s1.On("Start", mock.Anything).Return(nil) | ||
s1.On("Stop", mock.Anything).Return(stopErr) | ||
app := boot.NewApplicationForService(s1, 5*time.Second) | ||
|
||
cancel() | ||
err := app.Run(ctx) | ||
|
||
assert.ErrorIs(t, err, stopErr) | ||
} |