Skip to content

Commit

Permalink
refactor(shutdown): shutdown is now non-blocking
Browse files Browse the repository at this point in the history
  • Loading branch information
samber committed Dec 31, 2023
1 parent 59acf30 commit c8221b4
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 59 deletions.
15 changes: 10 additions & 5 deletions docs/docs/service-lifecycle/shutdowner.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ A shutdown can be triggered on a root scope:

```go
// on demand
injector.Shutdown() error
injector.ShutdownWithContext(context.Context) error
injector.Shutdown() map[string]error
injector.ShutdownWithContext(context.Context) map[string]error

// on signal
injector.ShutdownOnSignals(...os.Signal) (os.Signal, error)
injector.ShutdownOnSignalsWithContext(context.Context, ...os.Signal) (os.Signal, error)
injector.ShutdownOnSignals(...os.Signal) (os.Signal, map[string]error)
injector.ShutdownOnSignalsWithContext(context.Context, ...os.Signal) (os.Signal, map[string]error)
```

...on a single service:
Expand Down Expand Up @@ -86,5 +86,10 @@ Provide(i, ...)
Invoke(i, ...)

ctx := context.WithTimeout(10 * time.Second)
i.ShutdownWithContext(ctx)
errors := i.ShutdownWithContext(ctx)
for _, err := range errors {
if err != nil {
log.Println("shutdown error:", err)
}
}
```
6 changes: 4 additions & 2 deletions docs/docs/upgrading/from-v1-x-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ go mod tidy
find . -name '*.go' -type f -exec sed -i '' "s/*do.Injector/do.Injector/g" {} \;
```

## 3- `do.Shutdown****` output
## 3- Shutdown

`ShutdownOnSignals` used to return only 1 argument.
`do.ShutdownOnSignals` used to return only 1 argument.

```go
# from
Expand All @@ -58,6 +58,8 @@ signal, err := injector.ShutdownOnSignals(syscall.SIGTERM, os.Interrupt)

`injector.ShutdownOnSIGTERM()` has been removed. Use `injector.ShutdownOnSignals(syscall.SIGTERM)` instead.

`injector.Shutdown()` now returns a map of errors (`map[string]error`) and is non-blocking in case of failure of a single service.

## 4- Internal service naming

Internally, the DI container stores a service by its name (string) that represents its type. In `do@v1`, some developers reported collisions in service names, because the package name was not included.
Expand Down
6 changes: 4 additions & 2 deletions examples/shutdownable/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ func main() {
car.Start()

_, err := injector.ShutdownOnSignals()
if err != nil {
log.Fatal(err.Error())
for _, e := range err {
if e != nil {
log.Fatal(e.Error())
}
}
}
4 changes: 2 additions & 2 deletions injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ type Injector interface {
ListInvokedServices() []EdgeService
HealthCheck() map[string]error
HealthCheckWithContext(context.Context) map[string]error
Shutdown() error
ShutdownWithContext(context.Context) error
Shutdown() map[string]error
ShutdownWithContext(context.Context) map[string]error
clone(*RootScope, *Scope) *Scope

// service lifecycle
Expand Down
8 changes: 4 additions & 4 deletions root_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ func (s *RootScope) HealthCheck() map[string]error { return s.self.Heal
func (s *RootScope) HealthCheckWithContext(ctx context.Context) map[string]error {
return s.self.HealthCheckWithContext(ctx)
}
func (s *RootScope) Shutdown() error { return s.ShutdownWithContext(context.Background()) }
func (s *RootScope) ShutdownWithContext(ctx context.Context) error {
func (s *RootScope) Shutdown() map[string]error { return s.ShutdownWithContext(context.Background()) }
func (s *RootScope) ShutdownWithContext(ctx context.Context) map[string]error {
defer func() {
if s.healthCheckPool != nil {
s.healthCheckPool.stop()
Expand Down Expand Up @@ -142,14 +142,14 @@ func (s *RootScope) CloneWithOpts(opts *InjectorOpts) *RootScope {
// ShutdownOnSignals listens for signals defined in signals parameter in order to graceful stop service.
// It will block until receiving any of these signal.
// If no signal is provided in signals parameter, syscall.SIGTERM and os.Interrupt will be added as default signal.
func (s *RootScope) ShutdownOnSignals(signals ...os.Signal) (os.Signal, error) {
func (s *RootScope) ShutdownOnSignals(signals ...os.Signal) (os.Signal, map[string]error) {
return s.ShutdownOnSignalsWithContext(context.Background(), signals...)
}

// ShutdownOnSignalsWithContext listens for signals defined in signals parameter in order to graceful stop service.
// It will block until receiving any of these signal.
// If no signal is provided in signals parameter, syscall.SIGTERM and os.Interrupt will be added as default signal.
func (s *RootScope) ShutdownOnSignalsWithContext(ctx context.Context, signals ...os.Signal) (os.Signal, error) {
func (s *RootScope) ShutdownOnSignalsWithContext(ctx context.Context, signals ...os.Signal) (os.Signal, map[string]error) {
// Make sure there is at least syscall.SIGTERM and os.Interrupt as a signal
if len(signals) < 1 {
signals = append(signals, syscall.SIGTERM, os.Interrupt)
Expand Down
18 changes: 7 additions & 11 deletions scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,25 +212,24 @@ func (s *Scope) asyncHealthCheckWithContext(ctx context.Context) map[string]<-ch
}

// Shutdown shutdowns the scope and all its children.
func (s *Scope) Shutdown() error {
func (s *Scope) Shutdown() map[string]error {
return s.ShutdownWithContext(context.Background())
}

// ShutdownWithContext shutdowns the scope and all its children.
func (s *Scope) ShutdownWithContext(ctx context.Context) error {
func (s *Scope) ShutdownWithContext(ctx context.Context) map[string]error {
s.mu.RLock()
children := s.childScopes
invocations := invertMap(s.orderedInvocation)
s.mu.RUnlock()

s.logf("requested shutdown")

err := map[string]error{}

// first shutdown children
for k, child := range children {
err := child.Shutdown()
if err != nil {
return err
}
err = mergeMaps(err, child.Shutdown())

s.mu.Lock()
delete(s.childScopes, k) // scope is removed from DI container
Expand All @@ -244,15 +243,12 @@ func (s *Scope) ShutdownWithContext(ctx context.Context) error {
continue
}

err := s.serviceShutdown(ctx, name)
if err != nil {
return err
}
err[name] = s.serviceShutdown(ctx, name)
}

s.logf("shutdowned services")

return nil
return err
}

func (s *Scope) clone(root *RootScope, parent *Scope) *Scope {
Expand Down
71 changes: 40 additions & 31 deletions scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,16 @@ func TestScope_HealthCheckWithContext(t *testing.T) {
}

func TestScope_Shutdown(t *testing.T) {
// @TODO
is := assert.New(t)

i := New()

ProvideNamedValue(i, "lazy-ok", &lazyTestShutdownerOK{})
ProvideNamedValue(i, "lazy-ko", &lazyTestShutdownerKO{})
_, _ = InvokeNamed[*lazyTestShutdownerOK](i, "lazy-ok")
_, _ = InvokeNamed[*lazyTestShutdownerKO](i, "lazy-ko")

is.EqualValues(map[string]error{"lazy-ok": nil, "lazy-ko": assert.AnError}, i.Shutdown())
}

// @TODO: missing tests for context
Expand All @@ -426,11 +435,11 @@ func TestScope_ShutdownWithContext(t *testing.T) {
child2a := child1.Scope("child2a")
child2b := child1.Scope("child2b")

provider1 := func(i Injector) (*lazyTestHeathcheckerOK, error) {
return &lazyTestHeathcheckerOK{foobar: "foobar"}, nil
provider1 := func(i Injector) (*lazyTestShutdownerOK, error) {
return &lazyTestShutdownerOK{foobar: "foobar"}, nil
}
provider2 := func(i Injector) (*lazyTestHeathcheckerKO, error) {
return &lazyTestHeathcheckerKO{foobar: "foobar"}, nil
provider2 := func(i Injector) (*lazyTestShutdownerKO, error) {
return &lazyTestShutdownerKO{foobar: "foobar"}, nil
}

rootScope.serviceSet("root-a", newServiceLazy("root-a", provider2))
Expand All @@ -439,39 +448,39 @@ func TestScope_ShutdownWithContext(t *testing.T) {
child2a.serviceSet("child2a-b", newServiceLazy("child2a-b", provider2))
child2b.serviceSet("child2b-a", newServiceLazy("child2b-a", provider2))

_, _ = invokeByName[*lazyTestHeathcheckerKO](rootScope, "root-a")
_, _ = invokeByName[*lazyTestHeathcheckerOK](child1, "child1-a")
_, _ = invokeByName[*lazyTestHeathcheckerOK](child2a, "child2a-a")
_, _ = invokeByName[*lazyTestHeathcheckerKO](child2a, "child2a-b")
_, _ = invokeByName[*lazyTestHeathcheckerKO](child2b, "child2b-a")
_, _ = invokeByName[*lazyTestShutdownerKO](rootScope, "root-a")
_, _ = invokeByName[*lazyTestShutdownerOK](child1, "child1-a")
_, _ = invokeByName[*lazyTestShutdownerOK](child2a, "child2a-a")
_, _ = invokeByName[*lazyTestShutdownerKO](child2a, "child2a-b")
_, _ = invokeByName[*lazyTestShutdownerKO](child2b, "child2b-a")

// from rootScope POV
is.Equal(assert.AnError, rootScope.serviceHealthCheck(ctx, "root-a"))
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child1-a"), "could not find service")
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
is.Equal(assert.AnError, rootScope.serviceShutdown(ctx, "root-a"))
is.ErrorContains(rootScope.serviceShutdown(ctx, "child1-a"), "could not find service")
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2a-a"), "could not find service")
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2a-b"), "could not find service")
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2b-a"), "could not find service")

// from child1 POV
is.ErrorContains(child1.serviceHealthCheck(ctx, "root-a"), "could not find service")
is.Equal(nil, child1.serviceHealthCheck(ctx, "child1-a"))
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
is.ErrorContains(child1.serviceShutdown(ctx, "root-a"), "could not find service")
is.Equal(nil, child1.serviceShutdown(ctx, "child1-a"))
is.ErrorContains(child1.serviceShutdown(ctx, "child2a-a"), "could not find service")
is.ErrorContains(child1.serviceShutdown(ctx, "child2a-b"), "could not find service")
is.ErrorContains(child1.serviceShutdown(ctx, "child2b-a"), "could not find service")

// from child2a POV
is.ErrorContains(child2a.serviceHealthCheck(ctx, "root-a"), "could not find service")
is.ErrorContains(child2a.serviceHealthCheck(ctx, "child1-a"), "could not find service")
is.Equal(nil, child2a.serviceHealthCheck(ctx, "child2a-a"))
is.Equal(assert.AnError, child2a.serviceHealthCheck(ctx, "child2a-b"))
is.ErrorContains(child2a.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
is.ErrorContains(child2a.serviceShutdown(ctx, "root-a"), "could not find service")
is.ErrorContains(child2a.serviceShutdown(ctx, "child1-a"), "could not find service")
is.Equal(nil, child2a.serviceShutdown(ctx, "child2a-a"))
is.Equal(assert.AnError, child2a.serviceShutdown(ctx, "child2a-b"))
is.ErrorContains(child2a.serviceShutdown(ctx, "child2b-a"), "could not find service")

// from child2b POV
is.ErrorContains(child2b.serviceHealthCheck(ctx, "root-a"), "could not find service")
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child1-a"), "could not find service")
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
is.Equal(assert.AnError, child2b.serviceHealthCheck(ctx, "child2b-a"))
is.ErrorContains(child2b.serviceShutdown(ctx, "root-a"), "could not find service")
is.ErrorContains(child2b.serviceShutdown(ctx, "child1-a"), "could not find service")
is.ErrorContains(child2b.serviceShutdown(ctx, "child2a-a"), "could not find service")
is.ErrorContains(child2b.serviceShutdown(ctx, "child2a-b"), "could not find service")
is.Equal(assert.AnError, child2b.serviceShutdown(ctx, "child2b-a"))
}

func TestScope_clone(t *testing.T) {
Expand Down Expand Up @@ -531,7 +540,7 @@ func TestScope_serviceHealthCheck(t *testing.T) {
_, _ = invokeByName[int](child3, "child3-a")

is.ElementsMatch([]EdgeService{newEdgeService(child3.id, child3.name, "child3-a"), newEdgeService(child2a.id, child2a.name, "child2a-a"), newEdgeService(child2a.id, child2a.name, "child2a-b"), newEdgeService(child1.id, child1.name, "child1-a")}, child3.ListInvokedServices())
is.Nil(child1.Shutdown())
is.EqualValues(map[string]error{"child1-a": nil, "child2a-a": nil, "child2a-b": nil, "child2b-a": nil, "child3-a": nil}, child1.Shutdown())
is.ElementsMatch([]EdgeService{}, child3.ListInvokedServices())
}

Expand Down
12 changes: 12 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ func filter[V any](collection []V, predicate func(item V, index int) bool) []V {
return result
}

func mergeMaps[K comparable, V any](ins ...map[K]V) map[K]V {
out := map[K]V{}

for _, in := range ins {
for k, v := range in {
out[k] = v
}
}

return out
}

func invertMap[K comparable, V comparable](in map[K]V) map[V]K {
out := map[V]K{}

Expand Down
12 changes: 12 additions & 0 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ func TestUtilsMap(t *testing.T) {
is.Equal(result2, []string{"1", "2", "3", "4"})
}

func TestUtilsMergeMaps(t *testing.T) {
t.Parallel()
is := assert.New(t)

result1 := mergeMaps(map[string]int{"a": 1, "b": 2, "c": 3}, map[string]int{"c": 4, "d": 5, "e": 6})
result2 := mergeMaps[string, int]()

is.Equal(len(result1), 5)
is.Equal(len(result2), 0)
is.Equal(result1, map[string]int{"a": 1, "b": 2, "c": 4, "d": 5, "e": 6})
}

func TestUtilsInvertMap(t *testing.T) {
t.Parallel()
is := assert.New(t)
Expand Down
4 changes: 2 additions & 2 deletions virtual_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func (s *virtualScope) HealthCheck() map[string]error { return s.self.H
func (s *virtualScope) HealthCheckWithContext(ctx context.Context) map[string]error {
return s.self.HealthCheckWithContext(ctx)
}
func (s *virtualScope) Shutdown() error { return s.self.Shutdown() }
func (s *virtualScope) ShutdownWithContext(ctx context.Context) error {
func (s *virtualScope) Shutdown() map[string]error { return s.self.Shutdown() }
func (s *virtualScope) ShutdownWithContext(ctx context.Context) map[string]error {
return s.self.ShutdownWithContext(ctx)
}
func (s *virtualScope) clone(r *RootScope, p *Scope) *Scope { return s.self.clone(r, p) }
Expand Down

0 comments on commit c8221b4

Please sign in to comment.