full unit testing and fixes
- mapToString is ordered according to setup - setupSingle & teardownSingle no longer return "ok" states - teardown functions support errors - setup and teardown functions receive module
This commit is contained in:
143
lifecycle.go
143
lifecycle.go
@@ -1,16 +1,13 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// setupFn is a function that runs setup logic for a module.
|
||||
type setupFn func() error
|
||||
|
||||
// teardownFn is a function that runs teardown logic for a module.
|
||||
type teardownFn func()
|
||||
|
||||
// LifecycleOpts contains user-exposed options when defining a lifecycle.
|
||||
type LifecycleOpts struct {
|
||||
// DisableAutoload disables dependency autoloading (enabled by default).
|
||||
@@ -24,10 +21,13 @@ type Lifecycle struct {
|
||||
// by the lifecycle itself using the default logger.
|
||||
logger *slog.Logger
|
||||
|
||||
modules []*Module
|
||||
setupTracker map[string]bool
|
||||
teardownTracker map[string]bool
|
||||
opts LifecycleOpts
|
||||
modules []*Module
|
||||
opts LifecycleOpts
|
||||
|
||||
setupCount int
|
||||
setupTracker map[string]int
|
||||
teardownCount int
|
||||
teardownTracker map[string]int
|
||||
}
|
||||
|
||||
// NewLifecycle creates a new Lifecycle instance with a default logger and the
|
||||
@@ -56,8 +56,8 @@ func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle {
|
||||
return &Lifecycle{
|
||||
logger: defaultLogger,
|
||||
modules: modules,
|
||||
setupTracker: make(map[string]bool),
|
||||
teardownTracker: make(map[string]bool),
|
||||
setupTracker: make(map[string]int),
|
||||
teardownTracker: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,36 +80,37 @@ func (app *Lifecycle) Logger() *slog.Logger {
|
||||
// if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end
|
||||
// of the application lifecycle to ensure all resources are cleaned up properly.
|
||||
func (app *Lifecycle) Setup() error {
|
||||
setupCount := 0
|
||||
for _, mod := range app.modules {
|
||||
if ok, err := app.setupSingle(mod, app.logger); err != nil {
|
||||
if err := app.setupSingle(mod, app.logger); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
setupCount++
|
||||
}
|
||||
}
|
||||
|
||||
app.logger.Info("Lifecycle modules initialized, backend setup complete",
|
||||
"count", setupCount, "modules", mapToString(app.setupTracker))
|
||||
"modules", mapToString(app.setupTracker))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Teardown runs all module teardown functions in reverse order of setup.
|
||||
// Teardown should always be run at the end of the application lifecycle
|
||||
// to ensure all resources are cleaned up properly.
|
||||
// to ensure all resources are cleaned up properly. All module tear down
|
||||
// errors are returned as a single error (non-blocking).
|
||||
func (app *Lifecycle) Teardown() error {
|
||||
teardownCount := 0
|
||||
var err error
|
||||
for i := len(app.modules) - 1; i >= 0; i-- {
|
||||
if ok, err := app.singleTeardown(app.modules[i]); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
teardownCount++
|
||||
if singleErr := app.teardownSingle(app.modules[i]); singleErr != nil {
|
||||
err = errors.Join(err, singleErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
app.logger.Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.logger.Info("All modules torn down, backend teardown complete",
|
||||
"count", teardownCount, "modules", mapToString(app.teardownTracker))
|
||||
"modules", mapToString(app.teardownTracker))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -127,6 +128,14 @@ func (app *Lifecycle) Require(modules ...*Module) error {
|
||||
// This variation is useful when you need to set up modules with a non-
|
||||
// default logger.
|
||||
func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
if logger == nil {
|
||||
logger = app.logger
|
||||
}
|
||||
|
||||
if len(modules) == 0 {
|
||||
return fmt.Errorf("no modules to require")
|
||||
}
|
||||
|
||||
for _, mod := range modules {
|
||||
if mod == nil {
|
||||
return fmt.Errorf("module is nil")
|
||||
@@ -135,6 +144,8 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
// Check if the module is already set up
|
||||
if _, ok := app.setupTracker[mod.name]; ok {
|
||||
app.logger.Warn("module already set up, ignoring", "module", mod.name)
|
||||
mod.loaded = true
|
||||
mod.logger = logger
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -142,7 +153,7 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
app.modules = append(app.modules, mod)
|
||||
|
||||
// Run the setup function for the module
|
||||
if _, err := app.setupSingle(mod, logger); err != nil {
|
||||
if err := app.setupSingle(mod, logger); err != nil {
|
||||
return fmt.Errorf("error setting up required module '%s': %w", mod.name, err)
|
||||
}
|
||||
}
|
||||
@@ -152,10 +163,11 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupSingle is a helper function to set up a single module.
|
||||
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error) {
|
||||
// setupSingle is a helper function to set up a single module. Returns an error
|
||||
// if the module cannot be set up or if dependencies are not satisfied.
|
||||
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error {
|
||||
if mod == nil {
|
||||
return false, fmt.Errorf("module is nil")
|
||||
return fmt.Errorf("module is nil")
|
||||
}
|
||||
|
||||
// Set the logger for the module
|
||||
@@ -165,59 +177,59 @@ func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error
|
||||
for _, dep := range mod.depends {
|
||||
if _, ok := app.setupTracker[dep]; !ok {
|
||||
if app.opts.DisableAutoload {
|
||||
return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name)
|
||||
return fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name)
|
||||
} else {
|
||||
// Attempt to set up the dependency
|
||||
mod, err := app.getModuleByName(dep)
|
||||
depmod, err := app.getModuleByName(dep)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return fmt.Errorf("error getting dependency '%s' for '%s': %w", dep, mod.name, err)
|
||||
}
|
||||
|
||||
if _, err := app.setupSingle(mod, logger); err != nil {
|
||||
return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err)
|
||||
if err := app.setupSingle(depmod, logger); err != nil {
|
||||
return fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mod.setup != nil {
|
||||
// Run the setup function for the module
|
||||
if err := mod.setup(); err != nil {
|
||||
return false, fmt.Errorf("error initializing '%s': %w", mod.name, err)
|
||||
if err := mod.setup(mod); err != nil {
|
||||
return fmt.Errorf("error initializing '%s': %w", mod.name, err)
|
||||
}
|
||||
|
||||
// Mark this module as setup
|
||||
app.setupTracker[mod.name] = true
|
||||
mod.loaded = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
// Mark this module as setup
|
||||
app.setupTracker[mod.name] = app.setupCount
|
||||
app.setupCount++
|
||||
mod.loaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// singleTeardown is a helper function to tear down a single module.
|
||||
func (app *Lifecycle) singleTeardown(mod *Module) (bool, error) {
|
||||
// teardownSingle is a helper function to tear down a single module. Returns
|
||||
// an error if anything goes wrong.
|
||||
func (app *Lifecycle) teardownSingle(mod *Module) error {
|
||||
if mod == nil {
|
||||
return false, fmt.Errorf("module is nil")
|
||||
return fmt.Errorf("module is nil")
|
||||
}
|
||||
|
||||
// Check if the module was set up
|
||||
if _, ok := app.setupTracker[mod.name]; !ok {
|
||||
return false, nil
|
||||
return fmt.Errorf("module '%s' is not set up, cannot tear down", mod.name)
|
||||
}
|
||||
|
||||
// Run the teardown function for the module
|
||||
if mod.teardown != nil {
|
||||
mod.teardown()
|
||||
|
||||
// Mark this module as torn down
|
||||
app.teardownTracker[mod.name] = true
|
||||
mod.loaded = false
|
||||
return true, nil
|
||||
if err := mod.teardown(mod); err != nil {
|
||||
return fmt.Errorf("error tearing down '%s': %w", mod.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
// Mark this module as torn down
|
||||
app.teardownTracker[mod.name] = app.teardownCount
|
||||
app.teardownCount++
|
||||
mod.loaded = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// getModuleByName retrieves a module by its name from the lifecycle.
|
||||
@@ -227,21 +239,30 @@ func (app *Lifecycle) getModuleByName(name string) (*Module, error) {
|
||||
return mod, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("module '%s' not found", name)
|
||||
return nil, ErrModuleNotFound
|
||||
}
|
||||
|
||||
// mapToString converts a map to an opinionated string representation.
|
||||
func mapToString(m map[string]bool) string {
|
||||
// mapToString converts a map to an ordered, opinionated string representation.
|
||||
func mapToString(m map[string]int) string {
|
||||
if len(m) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
result := "["
|
||||
// Sort the map by value
|
||||
values := make([]int, 0, len(m))
|
||||
reverseMap := make(map[int]string, len(m))
|
||||
for k, v := range m {
|
||||
if v {
|
||||
result += fmt.Sprintf("'%s' ", k)
|
||||
values = append(values, v)
|
||||
reverseMap[v] = k
|
||||
}
|
||||
sort.Ints(values)
|
||||
|
||||
result := make([]string, 0, len(m))
|
||||
for _, v := range values {
|
||||
if name, ok := reverseMap[v]; ok {
|
||||
result = append(result, fmt.Sprintf("'%s'", name))
|
||||
}
|
||||
}
|
||||
result = result[:len(result)-1] + "]"
|
||||
return result
|
||||
|
||||
return "[" + strings.Join(result, " ") + "]"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user