246 lines
6.8 KiB
Go
246 lines
6.8 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
)
|
|
|
|
// 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).
|
|
DisableAutoload bool
|
|
}
|
|
|
|
// Lifecycle represents the core application structure. Lifecycle manages
|
|
// resources providing an orchestrator for setup and teardown of modules.
|
|
type Lifecycle struct {
|
|
// Logger is the logger for the lifecycle. If not set, it will be initialized
|
|
// by the lifecycle itself using the default logger.
|
|
logger *slog.Logger
|
|
|
|
modules []*Module
|
|
setupTracker map[string]bool
|
|
teardownTracker map[string]bool
|
|
opts LifecycleOpts
|
|
}
|
|
|
|
// NewLifecycle creates a new Lifecycle instance with a default logger and the
|
|
// given modules. It panics if any module has a duplicate name.
|
|
func NewLifecycle(modules ...*Module) *Lifecycle {
|
|
return NewLifecycleL(nil, modules...)
|
|
}
|
|
|
|
// NewLifecycleL creates a new Lifecycle instance with a specific logger and the
|
|
// given modules. See NewLifecycle for more details.
|
|
func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle {
|
|
// Use the provided logger or initialize a default one
|
|
if defaultLogger == nil {
|
|
defaultLogger = slog.Default()
|
|
}
|
|
|
|
// Ensure modules are unique
|
|
unique := make(map[string]bool)
|
|
for _, sub := range modules {
|
|
if _, exists := unique[sub.name]; exists {
|
|
panic(fmt.Sprintf("duplicate module name: %s", sub.name))
|
|
}
|
|
unique[sub.name] = true
|
|
}
|
|
|
|
return &Lifecycle{
|
|
logger: defaultLogger,
|
|
modules: modules,
|
|
setupTracker: make(map[string]bool),
|
|
teardownTracker: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// WithOpts sets the options for the lifecycle.
|
|
func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle {
|
|
app.opts = opts
|
|
return app
|
|
}
|
|
|
|
// Logger returns the logger for the lifecycle.
|
|
func (app *Lifecycle) Logger() *slog.Logger {
|
|
if app.logger == nil {
|
|
panic("lifecycle logger is not initialized")
|
|
}
|
|
return app.logger
|
|
}
|
|
|
|
// Setup initializes all modules in the order they were defined and checks
|
|
// for dependencies. It returns an error if any module fails to initialize or
|
|
// 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 _, sub := range app.modules {
|
|
if ok, err := app.setupSingle(sub, 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))
|
|
|
|
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.
|
|
func (app *Lifecycle) Teardown() error {
|
|
teardownCount := 0
|
|
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++
|
|
}
|
|
}
|
|
|
|
app.logger.Info("All modules torn down, backend teardown complete",
|
|
"count", teardownCount, "modules", mapToString(app.teardownTracker))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Require adds module(s) to the lifecycle and immediately runs any setup
|
|
// functions. Relevant when a module is not part of the main application
|
|
// but may still be conditionally necessary. Any modules that are already
|
|
// set up are ignored.
|
|
func (app *Lifecycle) Require(modules ...*Module) error {
|
|
return app.RequireL(app.logger, modules...)
|
|
}
|
|
|
|
// RequireL adds module(s) to the lifecycle with a specific logger and
|
|
// immediately runs any setup functions. See Require for more details.
|
|
// 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 {
|
|
for _, sub := range modules {
|
|
if sub == nil {
|
|
return fmt.Errorf("module is nil")
|
|
}
|
|
|
|
// Check if the module is already set up
|
|
if _, ok := app.setupTracker[sub.name]; ok {
|
|
app.logger.Warn("module already set up, ignoring", "module", sub.name)
|
|
continue
|
|
}
|
|
|
|
// Add the module to the lifecycle
|
|
app.modules = append(app.modules, sub)
|
|
|
|
// Run the setup function for the module
|
|
if _, err := app.setupSingle(sub, logger); err != nil {
|
|
return fmt.Errorf("error setting up required module '%s': %w", sub.name, err)
|
|
}
|
|
}
|
|
|
|
app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker))
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupSingle is a helper function to set up a single module.
|
|
func (app *Lifecycle) setupSingle(sub *Module, logger *slog.Logger) (bool, error) {
|
|
if sub == nil {
|
|
return false, fmt.Errorf("module is nil")
|
|
}
|
|
|
|
// Set the logger for the module
|
|
sub.logger = logger
|
|
|
|
// Check if all dependencies are satisfied
|
|
for _, dep := range sub.depends {
|
|
if _, ok := app.setupTracker[dep]; !ok {
|
|
if app.opts.DisableAutoload {
|
|
return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, sub.name)
|
|
} else {
|
|
// Attempt to set up the dependency
|
|
mod, err := app.getModuleByName(dep)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if _, err := app.setupSingle(mod, logger); err != nil {
|
|
return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, sub.name, err)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
if sub.setup != nil {
|
|
// Run the setup function for the module
|
|
if err := sub.setup(); err != nil {
|
|
return false, fmt.Errorf("error initializing '%s': %w", sub.name, err)
|
|
}
|
|
|
|
// Mark this module as setup
|
|
app.setupTracker[sub.name] = true
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// singleTeardown is a helper function to tear down a single module.
|
|
func (app *Lifecycle) singleTeardown(sub *Module) (bool, error) {
|
|
if sub == nil {
|
|
return false, fmt.Errorf("module is nil")
|
|
}
|
|
|
|
// Check if the module was set up
|
|
if _, ok := app.setupTracker[sub.name]; !ok {
|
|
return false, nil
|
|
}
|
|
|
|
// Run the teardown function for the module
|
|
if sub.teardown != nil {
|
|
sub.teardown()
|
|
|
|
// Mark this module as torn down
|
|
app.teardownTracker[sub.name] = true
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// getModuleByName retrieves a module by its name from the lifecycle.
|
|
func (app *Lifecycle) getModuleByName(name string) (*Module, error) {
|
|
for _, sub := range app.modules {
|
|
if sub.name == name {
|
|
return sub, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("module '%s' not found", name)
|
|
}
|
|
|
|
// mapToString converts a map to an opinionated string representation.
|
|
func mapToString(m map[string]bool) string {
|
|
if len(m) == 0 {
|
|
return "[]"
|
|
}
|
|
|
|
result := "["
|
|
for k, v := range m {
|
|
if v {
|
|
result += fmt.Sprintf("'%s' ", k)
|
|
}
|
|
}
|
|
result = result[:len(result)-1] + "]"
|
|
return result
|
|
}
|