Skip to content

Commit

Permalink
Added Application
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei Smirnov committed May 12, 2023
1 parent 444145b commit 91aba22
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 4 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ if err := services.Start(ctx); err != nil {
}
```

You can combine sequential and parallel boot flows. For example, given services A, B, C and D. Where service A must be started first, then B and C can be started simultaneously and then D can be started only after A, B and C have started:

```golang
services := boot.Sequentially(a, boot.Simultaneously(b, c), d)
err := services.Start(ctx)
```

5. Shutdown all services respecting a timeout

```golang
Expand All @@ -66,13 +73,19 @@ if err := services.Stop(ctx); err != nil {
}
```

# Advanced usage
# Using of `Application` wrapper

You can combine sequential and parallel boot flows. For example, given services A, B, C and D. Where service A must be started first, then B and C can be started simultaneously and then D can be started only after A, B and C have started:
Almost every application is handling OS signals to trigger a shutdown. The framework provides this convenient wrapper, that allows you to build a complete graceful application:

```golang
services := boot.Sequentially(a, boot.Simultaneously(b, c), d)
err := services.Start(ctx)
func main() {
// creating services: s1, s2, s3
services := boot.Sequentially(s1, s2, s3)
app := boot.NewApplicationForService(services)
if err := app.Run(context.Background(), 5 * time.Second); err != nil {
fmt.Println("application error:", err)
}
}
```

# License
Expand Down
52 changes: 52 additions & 0 deletions boot/application.go
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))
}
97 changes: 97 additions & 0 deletions boot/application_test.go
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)
}

0 comments on commit 91aba22

Please sign in to comment.