Skip to content

Commit c8221b4

Browse files
committed
refactor(shutdown): shutdown is now non-blocking
1 parent 59acf30 commit c8221b4

File tree

10 files changed

+97
-59
lines changed

10 files changed

+97
-59
lines changed

docs/docs/service-lifecycle/shutdowner.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ A shutdown can be triggered on a root scope:
1818

1919
```go
2020
// on demand
21-
injector.Shutdown() error
22-
injector.ShutdownWithContext(context.Context) error
21+
injector.Shutdown() map[string]error
22+
injector.ShutdownWithContext(context.Context) map[string]error
2323

2424
// on signal
25-
injector.ShutdownOnSignals(...os.Signal) (os.Signal, error)
26-
injector.ShutdownOnSignalsWithContext(context.Context, ...os.Signal) (os.Signal, error)
25+
injector.ShutdownOnSignals(...os.Signal) (os.Signal, map[string]error)
26+
injector.ShutdownOnSignalsWithContext(context.Context, ...os.Signal) (os.Signal, map[string]error)
2727
```
2828

2929
...on a single service:
@@ -86,5 +86,10 @@ Provide(i, ...)
8686
Invoke(i, ...)
8787

8888
ctx := context.WithTimeout(10 * time.Second)
89-
i.ShutdownWithContext(ctx)
89+
errors := i.ShutdownWithContext(ctx)
90+
for _, err := range errors {
91+
if err != nil {
92+
log.Println("shutdown error:", err)
93+
}
94+
}
9095
```

docs/docs/upgrading/from-v1-x-to-v2.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ go mod tidy
4444
find . -name '*.go' -type f -exec sed -i '' "s/*do.Injector/do.Injector/g" {} \;
4545
```
4646

47-
## 3- `do.Shutdown****` output
47+
## 3- Shutdown
4848

49-
`ShutdownOnSignals` used to return only 1 argument.
49+
`do.ShutdownOnSignals` used to return only 1 argument.
5050

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

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

61+
`injector.Shutdown()` now returns a map of errors (`map[string]error`) and is non-blocking in case of failure of a single service.
62+
6163
## 4- Internal service naming
6264

6365
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.

examples/shutdownable/example.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ func main() {
9595
car.Start()
9696

9797
_, err := injector.ShutdownOnSignals()
98-
if err != nil {
99-
log.Fatal(err.Error())
98+
for _, e := range err {
99+
if e != nil {
100+
log.Fatal(e.Error())
101+
}
100102
}
101103
}

injector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ type Injector interface {
1717
ListInvokedServices() []EdgeService
1818
HealthCheck() map[string]error
1919
HealthCheckWithContext(context.Context) map[string]error
20-
Shutdown() error
21-
ShutdownWithContext(context.Context) error
20+
Shutdown() map[string]error
21+
ShutdownWithContext(context.Context) map[string]error
2222
clone(*RootScope, *Scope) *Scope
2323

2424
// service lifecycle

root_scope.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ func (s *RootScope) HealthCheck() map[string]error { return s.self.Heal
6666
func (s *RootScope) HealthCheckWithContext(ctx context.Context) map[string]error {
6767
return s.self.HealthCheckWithContext(ctx)
6868
}
69-
func (s *RootScope) Shutdown() error { return s.ShutdownWithContext(context.Background()) }
70-
func (s *RootScope) ShutdownWithContext(ctx context.Context) error {
69+
func (s *RootScope) Shutdown() map[string]error { return s.ShutdownWithContext(context.Background()) }
70+
func (s *RootScope) ShutdownWithContext(ctx context.Context) map[string]error {
7171
defer func() {
7272
if s.healthCheckPool != nil {
7373
s.healthCheckPool.stop()
@@ -142,14 +142,14 @@ func (s *RootScope) CloneWithOpts(opts *InjectorOpts) *RootScope {
142142
// ShutdownOnSignals listens for signals defined in signals parameter in order to graceful stop service.
143143
// It will block until receiving any of these signal.
144144
// If no signal is provided in signals parameter, syscall.SIGTERM and os.Interrupt will be added as default signal.
145-
func (s *RootScope) ShutdownOnSignals(signals ...os.Signal) (os.Signal, error) {
145+
func (s *RootScope) ShutdownOnSignals(signals ...os.Signal) (os.Signal, map[string]error) {
146146
return s.ShutdownOnSignalsWithContext(context.Background(), signals...)
147147
}
148148

149149
// ShutdownOnSignalsWithContext listens for signals defined in signals parameter in order to graceful stop service.
150150
// It will block until receiving any of these signal.
151151
// If no signal is provided in signals parameter, syscall.SIGTERM and os.Interrupt will be added as default signal.
152-
func (s *RootScope) ShutdownOnSignalsWithContext(ctx context.Context, signals ...os.Signal) (os.Signal, error) {
152+
func (s *RootScope) ShutdownOnSignalsWithContext(ctx context.Context, signals ...os.Signal) (os.Signal, map[string]error) {
153153
// Make sure there is at least syscall.SIGTERM and os.Interrupt as a signal
154154
if len(signals) < 1 {
155155
signals = append(signals, syscall.SIGTERM, os.Interrupt)

scope.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,25 +212,24 @@ func (s *Scope) asyncHealthCheckWithContext(ctx context.Context) map[string]<-ch
212212
}
213213

214214
// Shutdown shutdowns the scope and all its children.
215-
func (s *Scope) Shutdown() error {
215+
func (s *Scope) Shutdown() map[string]error {
216216
return s.ShutdownWithContext(context.Background())
217217
}
218218

219219
// ShutdownWithContext shutdowns the scope and all its children.
220-
func (s *Scope) ShutdownWithContext(ctx context.Context) error {
220+
func (s *Scope) ShutdownWithContext(ctx context.Context) map[string]error {
221221
s.mu.RLock()
222222
children := s.childScopes
223223
invocations := invertMap(s.orderedInvocation)
224224
s.mu.RUnlock()
225225

226226
s.logf("requested shutdown")
227227

228+
err := map[string]error{}
229+
228230
// first shutdown children
229231
for k, child := range children {
230-
err := child.Shutdown()
231-
if err != nil {
232-
return err
233-
}
232+
err = mergeMaps(err, child.Shutdown())
234233

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

247-
err := s.serviceShutdown(ctx, name)
248-
if err != nil {
249-
return err
250-
}
246+
err[name] = s.serviceShutdown(ctx, name)
251247
}
252248

253249
s.logf("shutdowned services")
254250

255-
return nil
251+
return err
256252
}
257253

258254
func (s *Scope) clone(root *RootScope, parent *Scope) *Scope {

scope_test.go

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,16 @@ func TestScope_HealthCheckWithContext(t *testing.T) {
411411
}
412412

413413
func TestScope_Shutdown(t *testing.T) {
414-
// @TODO
414+
is := assert.New(t)
415+
416+
i := New()
417+
418+
ProvideNamedValue(i, "lazy-ok", &lazyTestShutdownerOK{})
419+
ProvideNamedValue(i, "lazy-ko", &lazyTestShutdownerKO{})
420+
_, _ = InvokeNamed[*lazyTestShutdownerOK](i, "lazy-ok")
421+
_, _ = InvokeNamed[*lazyTestShutdownerKO](i, "lazy-ko")
422+
423+
is.EqualValues(map[string]error{"lazy-ok": nil, "lazy-ko": assert.AnError}, i.Shutdown())
415424
}
416425

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

429-
provider1 := func(i Injector) (*lazyTestHeathcheckerOK, error) {
430-
return &lazyTestHeathcheckerOK{foobar: "foobar"}, nil
438+
provider1 := func(i Injector) (*lazyTestShutdownerOK, error) {
439+
return &lazyTestShutdownerOK{foobar: "foobar"}, nil
431440
}
432-
provider2 := func(i Injector) (*lazyTestHeathcheckerKO, error) {
433-
return &lazyTestHeathcheckerKO{foobar: "foobar"}, nil
441+
provider2 := func(i Injector) (*lazyTestShutdownerKO, error) {
442+
return &lazyTestShutdownerKO{foobar: "foobar"}, nil
434443
}
435444

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

442-
_, _ = invokeByName[*lazyTestHeathcheckerKO](rootScope, "root-a")
443-
_, _ = invokeByName[*lazyTestHeathcheckerOK](child1, "child1-a")
444-
_, _ = invokeByName[*lazyTestHeathcheckerOK](child2a, "child2a-a")
445-
_, _ = invokeByName[*lazyTestHeathcheckerKO](child2a, "child2a-b")
446-
_, _ = invokeByName[*lazyTestHeathcheckerKO](child2b, "child2b-a")
451+
_, _ = invokeByName[*lazyTestShutdownerKO](rootScope, "root-a")
452+
_, _ = invokeByName[*lazyTestShutdownerOK](child1, "child1-a")
453+
_, _ = invokeByName[*lazyTestShutdownerOK](child2a, "child2a-a")
454+
_, _ = invokeByName[*lazyTestShutdownerKO](child2a, "child2a-b")
455+
_, _ = invokeByName[*lazyTestShutdownerKO](child2b, "child2b-a")
447456

448457
// from rootScope POV
449-
is.Equal(assert.AnError, rootScope.serviceHealthCheck(ctx, "root-a"))
450-
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child1-a"), "could not find service")
451-
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
452-
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
453-
is.ErrorContains(rootScope.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
458+
is.Equal(assert.AnError, rootScope.serviceShutdown(ctx, "root-a"))
459+
is.ErrorContains(rootScope.serviceShutdown(ctx, "child1-a"), "could not find service")
460+
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2a-a"), "could not find service")
461+
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2a-b"), "could not find service")
462+
is.ErrorContains(rootScope.serviceShutdown(ctx, "child2b-a"), "could not find service")
454463

455464
// from child1 POV
456-
is.ErrorContains(child1.serviceHealthCheck(ctx, "root-a"), "could not find service")
457-
is.Equal(nil, child1.serviceHealthCheck(ctx, "child1-a"))
458-
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
459-
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
460-
is.ErrorContains(child1.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
465+
is.ErrorContains(child1.serviceShutdown(ctx, "root-a"), "could not find service")
466+
is.Equal(nil, child1.serviceShutdown(ctx, "child1-a"))
467+
is.ErrorContains(child1.serviceShutdown(ctx, "child2a-a"), "could not find service")
468+
is.ErrorContains(child1.serviceShutdown(ctx, "child2a-b"), "could not find service")
469+
is.ErrorContains(child1.serviceShutdown(ctx, "child2b-a"), "could not find service")
461470

462471
// from child2a POV
463-
is.ErrorContains(child2a.serviceHealthCheck(ctx, "root-a"), "could not find service")
464-
is.ErrorContains(child2a.serviceHealthCheck(ctx, "child1-a"), "could not find service")
465-
is.Equal(nil, child2a.serviceHealthCheck(ctx, "child2a-a"))
466-
is.Equal(assert.AnError, child2a.serviceHealthCheck(ctx, "child2a-b"))
467-
is.ErrorContains(child2a.serviceHealthCheck(ctx, "child2b-a"), "could not find service")
472+
is.ErrorContains(child2a.serviceShutdown(ctx, "root-a"), "could not find service")
473+
is.ErrorContains(child2a.serviceShutdown(ctx, "child1-a"), "could not find service")
474+
is.Equal(nil, child2a.serviceShutdown(ctx, "child2a-a"))
475+
is.Equal(assert.AnError, child2a.serviceShutdown(ctx, "child2a-b"))
476+
is.ErrorContains(child2a.serviceShutdown(ctx, "child2b-a"), "could not find service")
468477

469478
// from child2b POV
470-
is.ErrorContains(child2b.serviceHealthCheck(ctx, "root-a"), "could not find service")
471-
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child1-a"), "could not find service")
472-
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child2a-a"), "could not find service")
473-
is.ErrorContains(child2b.serviceHealthCheck(ctx, "child2a-b"), "could not find service")
474-
is.Equal(assert.AnError, child2b.serviceHealthCheck(ctx, "child2b-a"))
479+
is.ErrorContains(child2b.serviceShutdown(ctx, "root-a"), "could not find service")
480+
is.ErrorContains(child2b.serviceShutdown(ctx, "child1-a"), "could not find service")
481+
is.ErrorContains(child2b.serviceShutdown(ctx, "child2a-a"), "could not find service")
482+
is.ErrorContains(child2b.serviceShutdown(ctx, "child2a-b"), "could not find service")
483+
is.Equal(assert.AnError, child2b.serviceShutdown(ctx, "child2b-a"))
475484
}
476485

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

533542
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())
534-
is.Nil(child1.Shutdown())
543+
is.EqualValues(map[string]error{"child1-a": nil, "child2a-a": nil, "child2a-b": nil, "child2b-a": nil, "child3-a": nil}, child1.Shutdown())
535544
is.ElementsMatch([]EdgeService{}, child3.ListInvokedServices())
536545
}
537546

utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ func filter[V any](collection []V, predicate func(item V, index int) bool) []V {
8686
return result
8787
}
8888

89+
func mergeMaps[K comparable, V any](ins ...map[K]V) map[K]V {
90+
out := map[K]V{}
91+
92+
for _, in := range ins {
93+
for k, v := range in {
94+
out[k] = v
95+
}
96+
}
97+
98+
return out
99+
}
100+
89101
func invertMap[K comparable, V comparable](in map[K]V) map[V]K {
90102
out := map[V]K{}
91103

utils_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ func TestUtilsMap(t *testing.T) {
9797
is.Equal(result2, []string{"1", "2", "3", "4"})
9898
}
9999

100+
func TestUtilsMergeMaps(t *testing.T) {
101+
t.Parallel()
102+
is := assert.New(t)
103+
104+
result1 := mergeMaps(map[string]int{"a": 1, "b": 2, "c": 3}, map[string]int{"c": 4, "d": 5, "e": 6})
105+
result2 := mergeMaps[string, int]()
106+
107+
is.Equal(len(result1), 5)
108+
is.Equal(len(result2), 0)
109+
is.Equal(result1, map[string]int{"a": 1, "b": 2, "c": 4, "d": 5, "e": 6})
110+
}
111+
100112
func TestUtilsInvertMap(t *testing.T) {
101113
t.Parallel()
102114
is := assert.New(t)

virtual_scope.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ func (s *virtualScope) HealthCheck() map[string]error { return s.self.H
3030
func (s *virtualScope) HealthCheckWithContext(ctx context.Context) map[string]error {
3131
return s.self.HealthCheckWithContext(ctx)
3232
}
33-
func (s *virtualScope) Shutdown() error { return s.self.Shutdown() }
34-
func (s *virtualScope) ShutdownWithContext(ctx context.Context) error {
33+
func (s *virtualScope) Shutdown() map[string]error { return s.self.Shutdown() }
34+
func (s *virtualScope) ShutdownWithContext(ctx context.Context) map[string]error {
3535
return s.self.ShutdownWithContext(ctx)
3636
}
3737
func (s *virtualScope) clone(r *RootScope, p *Scope) *Scope { return s.self.clone(r, p) }

0 commit comments

Comments
 (0)