From f8bb2b99b34d5a1ef8b286fa43af350f2ce35ce2 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Fri, 8 Sep 2023 15:55:19 -0300 Subject: [PATCH] :sparkles: (HOOKS) Create code to introduce hooks --- .editorconfig | 9 ++ .gitignore | 61 +++++++++++ README.md | 54 ++++++++++ container.go | 110 +++++++++++-------- container_test.go | 220 +++++++++++++++++++++++++++----------- errors.go => errs/errs.go | 66 +++++++++++- facades.go | 17 +++ hooks/doc.go | 19 ++++ hooks/hooks.go | 78 ++++++++++++++ 9 files changed, 527 insertions(+), 107 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore rename errors.go => errs/errs.go (57%) create mode 100644 facades.go create mode 100644 hooks/doc.go create mode 100644 hooks/hooks.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1a62086 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dd3e3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,git +# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,git + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,git \ No newline at end of file diff --git a/README.md b/README.md index eb5ef5e..81881b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # 🌩 Zeus - Simple Dependency Injection Container ![GitHub](https://img.shields.io/github/license/otoru/zeus) +![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/otoru/zeus) [![codecov](https://codecov.io/gh/Otoru/zeus/graph/badge.svg?token=Yfkyp5NZsY)](https://codecov.io/gh/Otoru/zeus) Zeus is a sleek and efficient dependency injection container for Go. Easily register "factories" (functions that create instances of types) and let zeus resolve those dependencies at runtime. @@ -21,6 +22,10 @@ Register your dependencies and let zeus handle the rest. Zeus detects and reports cycles in your dependencies to prevent runtime errors. +### 🪝 Hooks + +Zeus supports lifecycle hooks, allowing you to execute functions at the start and end of your application. This is especially useful for setups and teardowns, like establishing a database connection or gracefully shutting down services. + ## 🚀 Getting Started ### Installation @@ -56,6 +61,55 @@ err := c.Run(func(s string) error { }) ``` +### Using Hooks + +Zeus allows you to register hooks that run at the start and end of your application. This is useful for setting up and tearing down resources. + +```go +c := zeus.New() + +// Servoce is a dummy service that depends on Hooks. +type Service struct{} + +c.Provide(func(h zeus.Hooks) *Service { + h.OnStart(func() error { + fmt.Println("Starting up...") + return nil + }) + + h.OnStop(func() error { + fmt.Println("Shutting down...") + return nil + }) + return &Service{} +}) + +c.Run(func(s *Service) { + fmt.Println("Main function running with the service!") +}) + +// Outputs: +// Starting up... +// Main function running with the service! +// Shutting down... + +``` + +### Error Handling + +Zeus uses `ErrorSet` to aggregate multiple errors. This is especially useful when multiple errors occur during the lifecycle of your application, such as during dependency resolution or hook execution. + +An ErrorSet can be returned from the Run method. Here's how you can handle it: + +```go +err := c.Run(func() { /* ... */ }) +if es, ok := err.(*zeus.ErrorSet); ok { + for _, e := range es.Errors() { + fmt.Println(e) + } +} +``` + ## 🤝 Contributing Contributions are warmly welcomed! Please open a PR or an issue if you find any problems or have enhancement suggestions. diff --git a/container.go b/container.go index 3ff4a28..5159470 100644 --- a/container.go +++ b/container.go @@ -3,6 +3,8 @@ package zeus import ( "reflect" + "github.com/otoru/zeus/errs" + "github.com/otoru/zeus/hooks" "golang.org/x/exp/slices" ) @@ -10,6 +12,7 @@ import ( type Container struct { providers map[reflect.Type]reflect.Value instances map[reflect.Type]reflect.Value + hooks Hooks } // New initializes and returns a new instance of the Container. @@ -18,56 +21,24 @@ type Container struct { // // c := zeus.New() func New() *Container { + hooks := new(hooks.HooksImpl) providers := make(map[reflect.Type]reflect.Value, 0) instances := make(map[reflect.Type]reflect.Value, 0) container := new(Container) + container.hooks = hooks container.providers = providers container.instances = instances return container } -// Provide registers a factory function for dependency resolution. -// It ensures that the factory is a function, has a valid return type, and checks for duplicate factories. -// Returns an error if any of these conditions are not met. -// -// Example: -// -// c := zeus.New() -// c.Provide(func() int { return 42 }) -func (c *Container) Provide(factory interface{}) error { - factoryType := reflect.TypeOf(factory) - - if factoryType.Kind() != reflect.Func { - return NotAFunctionError{} - } - - if numOut := factoryType.NumOut(); numOut < 1 || numOut > 2 { - return InvalidFactoryReturnError{NumReturns: numOut} - } - - if factoryType.NumOut() == 2 && factoryType.Out(1).Name() != "error" { - return UnexpectedReturnTypeError{TypeName: factoryType.Out(1).Name()} - } - - serviceType := factoryType.Out(0) - - if _, exists := c.providers[serviceType]; exists { - return FactoryAlreadyProvidedError{TypeName: serviceType.Name()} - } - - c.providers[serviceType] = reflect.ValueOf(factory) - - return nil -} - // resolve attempts to resolve a dependency of the given type. // It checks for cyclic dependencies and ensures that all dependencies can be resolved. // Returns the resolved value and any error encountered during resolution. func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value, error) { if slices.Contains(stack, t) { - return reflect.Value{}, CyclicDependencyError{TypeName: t.Name()} + return reflect.Value{}, errs.CyclicDependencyError{TypeName: t.Name()} } if instance, exists := c.instances[t]; exists { @@ -77,7 +48,7 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value provider, ok := c.providers[t] if !ok { - return reflect.Value{}, DependencyResolutionError{TypeName: t.Name()} + return reflect.Value{}, errs.DependencyResolutionError{TypeName: t.Name()} } providerType := provider.Type() @@ -85,6 +56,12 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value for i := range dependencies { argType := providerType.In(i) + + if argType.Implements(reflect.TypeOf((*Hooks)(nil)).Elem()) { + dependencies[i] = reflect.ValueOf(c.hooks) + continue + } + argValue, err := c.resolve(argType, append(stack, t)) if err != nil { @@ -105,6 +82,40 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value return results[0], nil } +// Provide registers a factory function for dependency resolution. +// It ensures that the factory is a function, has a valid return type, and checks for duplicate factories. +// Returns an error if any of these conditions are not met. +// +// Example: +// +// c := zeus.New() +// c.Provide(func() int { return 42 }) +func (c *Container) Provide(factory interface{}) error { + factoryType := reflect.TypeOf(factory) + + if factoryType.Kind() != reflect.Func { + return errs.NotAFunctionError{} + } + + if numOut := factoryType.NumOut(); numOut < 1 || numOut > 2 { + return errs.InvalidFactoryReturnError{NumReturns: numOut} + } + + if factoryType.NumOut() == 2 && factoryType.Out(1).Name() != "error" { + return errs.UnexpectedReturnTypeError{TypeName: factoryType.Out(1).Name()} + } + + serviceType := factoryType.Out(0) + + if _, exists := c.providers[serviceType]; exists { + return errs.FactoryAlreadyProvidedError{TypeName: serviceType.Name()} + } + + c.providers[serviceType] = reflect.ValueOf(factory) + + return nil +} + // Run executes the provided function by resolving and injecting its dependencies. // It ensures that the function has a valid signature and that all dependencies can be resolved. // Returns an error if the function signature is invalid or if dependencies cannot be resolved. @@ -117,18 +128,20 @@ func (c *Container) resolve(t reflect.Type, stack []reflect.Type) (reflect.Value // fmt.Println(i) // Outputs: 42 // }) func (c *Container) Run(fn interface{}) error { + errorSet := &errs.ErrorSet{} + fnType := reflect.TypeOf(fn) if fnType.Kind() != reflect.Func { - return NotAFunctionError{} + return errs.NotAFunctionError{} } if numOut := fnType.NumOut(); numOut > 1 { - return InvalidFactoryReturnError{NumReturns: numOut} + return errs.InvalidFactoryReturnError{NumReturns: numOut} } if fnType.NumOut() == 1 && fnType.Out(0).Name() != "error" { - return UnexpectedReturnTypeError{TypeName: fnType.Out(0).Name()} + return errs.UnexpectedReturnTypeError{TypeName: fnType.Out(0).Name()} } dependencies := make([]reflect.Value, fnType.NumIn()) @@ -138,17 +151,30 @@ func (c *Container) Run(fn interface{}) error { argValue, err := c.resolve(argType, nil) if err != nil { - return err + errorSet.Add(err) + break } dependencies[i] = argValue } + if !errorSet.IsEmpty() { + return errorSet.Result() + } + + if err := c.hooks.Start(); err != nil { + errorSet.Add(err) + } + results := reflect.ValueOf(fn).Call(dependencies) if fnType.NumOut() == 1 && !results[0].IsNil() { - return results[0].Interface().(error) + errorSet.Add(results[0].Interface().(error)) } - return nil + if err := c.hooks.Stop(); err != nil { + errorSet.Add(err) + } + + return errorSet.Result() } diff --git a/container_test.go b/container_test.go index d7f81bb..1b4d6c3 100644 --- a/container_test.go +++ b/container_test.go @@ -1,73 +1,37 @@ package zeus import ( + "errors" "fmt" "reflect" + "strings" "testing" + "github.com/otoru/zeus/errs" "gotest.tools/v3/assert" ) func TestContainer(t *testing.T) { t.Parallel() - t.Run("Provide", func(t *testing.T) { - t.Run("Not a function", func(t *testing.T) { - c := New() - got := c.Provide("string") - expected := NotAFunctionError{} - assert.ErrorIs(t, got, expected) - }) - - t.Run("Invalid return count", func(t *testing.T) { - c := New() - got := c.Provide(func() (int, string, error) { return 0, "", nil }) - expected := InvalidFactoryReturnError{NumReturns: 3} - - assert.ErrorIs(t, got, expected) - }) - - t.Run("Second return value not is a error", func(t *testing.T) { - c := New() - got := c.Provide(func() (int, string) { return 0, "" }) - expected := UnexpectedReturnTypeError{TypeName: "string"} - - assert.ErrorIs(t, got, expected) - }) - - t.Run("Valid factory", func(t *testing.T) { - c := New() - err := c.Provide(func() int { return 0 }) - - assert.NilError(t, err) - }) - - t.Run("Duplicated factory", func(t *testing.T) { - c := New() - c.Provide(func() int { return 0 }) - got := c.Provide(func() int { return 1 }) - expected := FactoryAlreadyProvidedError{TypeName: "int"} - - assert.ErrorIs(t, got, expected) - }) - }) - t.Run("resolve", func(t *testing.T) { + t.Parallel() + t.Run("Cyclic dependency", func(t *testing.T) { c := New() c.Provide(func(s string) string { return s }) - _, err := c.resolve(reflect.TypeOf(""), []reflect.Type{reflect.TypeOf("")}) - expected := CyclicDependencyError{TypeName: "string"} + _, got := c.resolve(reflect.TypeOf(""), []reflect.Type{reflect.TypeOf("")}) + expected := errs.CyclicDependencyError{TypeName: "string"} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Unresolved dependency", func(t *testing.T) { c := New() - _, err := c.resolve(reflect.TypeOf(0.0), nil) - expected := DependencyResolutionError{TypeName: "float64"} + _, got := c.resolve(reflect.TypeOf(0.0), nil) + expected := errs.DependencyResolutionError{TypeName: "float64"} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Successful resolution", func(t *testing.T) { @@ -83,10 +47,10 @@ func TestContainer(t *testing.T) { t.Run("Recursive Call Error - Unresolved Dependency", func(t *testing.T) { c := New() c.Provide(func(f float64) int { return int(f) }) - _, err := c.resolve(reflect.TypeOf(0), nil) - expected := DependencyResolutionError{TypeName: "float64"} + _, got := c.resolve(reflect.TypeOf(0), nil) + expected := errs.DependencyResolutionError{TypeName: "float64"} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Recursive call error - cyclic dependency", func(t *testing.T) { @@ -94,7 +58,7 @@ func TestContainer(t *testing.T) { c.Provide(func(s string) int { return len(s) }) c.Provide(func(i int) string { return fmt.Sprint(i) }) _, err := c.resolve(reflect.TypeOf(0), nil) - expected := CyclicDependencyError{TypeName: "int"} + expected := errs.CyclicDependencyError{TypeName: "int"} assert.ErrorIs(t, err, expected) }) @@ -140,31 +104,108 @@ func TestContainer(t *testing.T) { assert.Equal(t, a.C, b.C) }) + t.Run("Hooks Injection", func(t *testing.T) { + c := New() + + err := c.Provide(func(h Hooks) *strings.Builder { + h.OnStart(func() error { + return nil + }) + + return &strings.Builder{} + }) + + assert.NilError(t, err) + + val, err := c.resolve(reflect.TypeOf(&strings.Builder{}), nil) + assert.NilError(t, err) + + _, ok := val.Interface().(*strings.Builder) + assert.Assert(t, ok) + }) + + }) + + t.Run("Provide", func(t *testing.T) { + t.Parallel() + + t.Run("Not a function", func(t *testing.T) { + c := New() + got := c.Provide("string") + expected := errs.NotAFunctionError{} + assert.ErrorIs(t, got, expected) + }) + + t.Run("Invalid return count", func(t *testing.T) { + c := New() + got := c.Provide(func() (int, string, error) { return 0, "", nil }) + expected := errs.InvalidFactoryReturnError{NumReturns: 3} + + assert.ErrorIs(t, got, expected) + }) + + t.Run("Second return value not is a error", func(t *testing.T) { + c := New() + got := c.Provide(func() (int, string) { return 0, "" }) + expected := errs.UnexpectedReturnTypeError{TypeName: "string"} + + assert.ErrorIs(t, got, expected) + }) + + t.Run("Valid factory", func(t *testing.T) { + c := New() + err := c.Provide(func() int { return 0 }) + + assert.NilError(t, err) + }) + + t.Run("Duplicated factory", func(t *testing.T) { + c := New() + c.Provide(func() int { return 0 }) + got := c.Provide(func() int { return 1 }) + expected := errs.FactoryAlreadyProvidedError{TypeName: "int"} + + assert.ErrorIs(t, got, expected) + }) + + t.Run("Hooks Injection", func(t *testing.T) { + c := New() + + err := c.Provide(func(h Hooks) *strings.Builder { + return &strings.Builder{} + }) + assert.NilError(t, err) + + _, exists := c.providers[reflect.TypeOf(&strings.Builder{})] + assert.Assert(t, exists) + }) }) t.Run("Run", func(t *testing.T) { + t.Parallel() + t.Run("Not a function", func(t *testing.T) { c := New() - err := c.Run("not a function") - expected := NotAFunctionError{} + got := c.Run("not a function") + expected := errs.NotAFunctionError{} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Invalid return", func(t *testing.T) { c := New() - err := c.Run(func() (int, string) { return 0, "" }) - expected := InvalidFactoryReturnError{NumReturns: 2} + got := c.Run(func() (int, string) { return 0, "" }) + expected := errs.InvalidFactoryReturnError{NumReturns: 2} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Function returns a non-error value", func(t *testing.T) { c := New() - err := c.Run(func() int { return 42 }) - expected := UnexpectedReturnTypeError{TypeName: "int"} + got := c.Run(func() int { return 42 }) + expected := errs.UnexpectedReturnTypeError{TypeName: "int"} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) }) t.Run("Successful execution", func(t *testing.T) { @@ -193,11 +234,64 @@ func TestContainer(t *testing.T) { t.Run("Dependency resolution error", func(t *testing.T) { c := New() - err := c.Run(func(f float64) error { return nil }) - expected := DependencyResolutionError{TypeName: "float64"} + got := c.Run(func(f float64) error { return nil }) + expected := errs.DependencyResolutionError{TypeName: "float64"} - assert.ErrorIs(t, err, expected) + assert.ErrorIs(t, got, expected) + }) + + t.Run("Successful Execution with Hooks", func(t *testing.T) { + c := New() + + started := false + stopped := false + + c.Provide(func(h Hooks) int { + h.OnStart(func() error { + started = true + return nil + }) + h.OnStop(func() error { + stopped = true + return nil + }) + return 42 + }) + + err := c.Run(func(number int) {}) + + assert.NilError(t, err) + assert.Assert(t, started) + assert.Assert(t, stopped) + }) + + t.Run("Error in OnStart Hook", func(t *testing.T) { + c := New() + + c.Provide(func(h Hooks) int { + h.OnStart(func() error { + return errors.New("start error") + }) + return 42 + }) + + err := c.Run(func(number int) {}) + assert.ErrorContains(t, err, "start error") }) + t.Run("Error in OnStop Hook", func(t *testing.T) { + c := New() + + c.Provide(func(h Hooks) int { + h.OnStop(func() error { + return errors.New("stop error") + }) + + return 42 + }) + + err := c.Run(func(number int) {}) + assert.ErrorContains(t, err, "stop error") + }) }) } diff --git a/errors.go b/errs/errs.go similarity index 57% rename from errors.go rename to errs/errs.go index ea91a1f..e1bfabf 100644 --- a/errors.go +++ b/errs/errs.go @@ -1,6 +1,11 @@ -package zeus +package errs -import "fmt" +import ( + "fmt" + "slices" + "strings" + "sync" +) // NotAFunctionError indicates that the provided object is not a function. type NotAFunctionError struct{} @@ -59,3 +64,60 @@ type CyclicDependencyError struct { func (e CyclicDependencyError) Error() string { return fmt.Sprintf("cyclic dependency detected for type %s", e.TypeName) } + +// ErrorSet is a collection of errors. +// It can be used to accumulate errors and retrieve them as a single error or a list. +type ErrorSet struct { + mu sync.Mutex + errors []error +} + +// Add appends an error to the error set. +func (es *ErrorSet) Add(err error) { + es.mu.Lock() + defer es.mu.Unlock() + es.errors = append(es.errors, err) +} + +// Errors returns the list of errors in the error set. +func (es *ErrorSet) Errors() []error { + slices.Reverse(es.errors) + return es.errors +} + +// Error implements the error interface. +// It returns a concatenated string of all error messages in the error set. +func (es *ErrorSet) Error() string { + errMsgs := []string{} + for _, err := range es.Errors() { + errMsgs = append(errMsgs, err.Error()) + } + return strings.Join(errMsgs, "; ") +} + +// Result returns a single error if there's only one error in the set, +// the ErrorSet itself if there's more than one error, or nil if there are no errors. +// Example: +// +// errSet := &ErrorSet{} +// errSet.Add(errors.New("First error")) +// errSet.Add(errors.New("Second error")) +// err := errSet.Result() +// fmt.Println(err) // Outputs: "First error; Second error" +func (es *ErrorSet) Result() error { + if len(es.errors) == 1 { + return es.errors[0] + } + + if len(es.errors) > 1 { + return es + } + + return nil +} + +// IsEmpty checks if the ErrorSet has no errors. +// It returns true if the ErrorSet is empty, otherwise false. +func (me *ErrorSet) IsEmpty() bool { + return len(me.errors) == 0 +} diff --git a/facades.go b/facades.go new file mode 100644 index 0000000..f7061d8 --- /dev/null +++ b/facades.go @@ -0,0 +1,17 @@ +package zeus + +import ( + "github.com/otoru/zeus/hooks" +) + +// Hooks is a facade for hooks.Hooks +type Hooks hooks.Hooks + +// ErrorSet is a facade for errs.ErrorSet +type ErrorSet interface { + IsEmpty() bool + Result() error + Error() string + Errors() []error + Add(err error) +} diff --git a/hooks/doc.go b/hooks/doc.go new file mode 100644 index 0000000..1e4bf63 --- /dev/null +++ b/hooks/doc.go @@ -0,0 +1,19 @@ +// Hooks defines an interface for a lifecycle of container. +// Example of using the Zeus container with Hooks and ErrorSet: +// +// c := zeus.New() +// +// c.Provide(func(h Hooks) *sql.DB { +// db := &sql.DB{} // pseudo-implementation +// +// h.OnStart(func() error { +// return errors.New("Failed to authenticate") +// }) +// +// h.OnStop(func() error { +// return errors.New("Failed to close the database") +// }) +// +// return db +// }) +package hooks diff --git a/hooks/hooks.go b/hooks/hooks.go new file mode 100644 index 0000000..d9adef1 --- /dev/null +++ b/hooks/hooks.go @@ -0,0 +1,78 @@ +package hooks + +import ( + "sync" + + "github.com/otoru/zeus/errs" +) + +// Hooks defines an interface for lifecycle events. +// It provides methods to register functions that should be executed +// at the start and stop of the application. +type Hooks interface { + OnStart(func() error) + OnStop(func() error) + Start() error + Stop() error +} + +// HooksImpl is the default implementation of the Hooks interface. +type HooksImpl struct { + onStart []func() error + onStop []func() error +} + +// OnStart adds a function to the list of functions to be executed at the start. +// Example: +// +// hooks.OnStart(func() error { +// fmt.Println("Starting...") +// return nil +// }) +func (h *HooksImpl) OnStart(fn func() error) { + h.onStart = append(h.onStart, fn) +} + +// OnStop adds a function to the list of functions to be executed at the stop. +// Example: +// +// hooks.OnStop(func() error { +// fmt.Println("Stopping...") +// return nil +// }) +func (h *HooksImpl) OnStop(fn func() error) { + h.onStop = append(h.onStop, fn) +} + +// Start executes all the registered OnStart hooks. +// It returns the first error encountered or nil if all hooks execute successfully. +// This method is internally used by the Container's Run function. +func (h *HooksImpl) Start() error { + for _, hook := range h.onStart { + if err := hook(); err != nil { + return err + } + } + return nil +} + +// Stop executes all the registered OnStop hooks. +// It returns the first error encountered or nil if all hooks execute successfully. +// This method is internally used by the Container's Run function. +func (h *HooksImpl) Stop() error { + var wg sync.WaitGroup + errorSet := &errs.ErrorSet{} + + for _, hook := range h.onStop { + wg.Add(1) + go func(hook func() error) { + defer wg.Done() + if err := hook(); err != nil { + errorSet.Add(err) + } + }(hook) + } + + wg.Wait() + return errorSet.Result() +}