rename Subsystem -> Module, add NewLifecycleL

This commit is contained in:
Elijah Duffy
2025-05-30 15:41:36 -07:00
parent 231561c926
commit b760b035b4
3 changed files with 112 additions and 93 deletions

View File

@@ -5,10 +5,10 @@ import (
"log/slog" "log/slog"
) )
// setupFn is a function that runs setup logic for a sub-system. // setupFn is a function that runs setup logic for a module.
type setupFn func() error type setupFn func() error
// teardownFn is a function that runs teardown logic for a sub-system. // teardownFn is a function that runs teardown logic for a module.
type teardownFn func() type teardownFn func()
// LifecycleOpts contains user-exposed options when defining a lifecycle. // LifecycleOpts contains user-exposed options when defining a lifecycle.
@@ -18,38 +18,44 @@ type LifecycleOpts struct {
} }
// Lifecycle represents the core application structure. Lifecycle manages // Lifecycle represents the core application structure. Lifecycle manages
// resources providing an orchestrator for setup and teardown of sub-systems. // resources providing an orchestrator for setup and teardown of modules.
type Lifecycle struct { type Lifecycle struct {
// Logger is the logger for the lifecycle. If not set, it will be initialized // Logger is the logger for the lifecycle. If not set, it will be initialized
// by the lifecycle itself using the default logger. // by the lifecycle itself using the default logger.
logger *slog.Logger logger *slog.Logger
subsystems []*Subsystem modules []*Module
setupTracker map[string]bool setupTracker map[string]bool
teardownTracker map[string]bool teardownTracker map[string]bool
opts LifecycleOpts opts LifecycleOpts
} }
// NewLifecycle creates a new Lifecycle instance with a default logger and the // NewLifecycle creates a new Lifecycle instance with a default logger and the
// given subsystems. It panics if any subsystem has a duplicate name. // given modules. It panics if any module has a duplicate name.
func NewLifecycle(defaultLogger *slog.Logger, subsystems ...*Subsystem) *Lifecycle { 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 // Use the provided logger or initialize a default one
if defaultLogger == nil { if defaultLogger == nil {
defaultLogger = slog.Default() defaultLogger = slog.Default()
} }
// Ensure subsystems are unique // Ensure modules are unique
unique := make(map[string]bool) unique := make(map[string]bool)
for _, sub := range subsystems { for _, sub := range modules {
if _, exists := unique[sub.name]; exists { if _, exists := unique[sub.name]; exists {
panic(fmt.Sprintf("duplicate subsystem name: %s", sub.name)) panic(fmt.Sprintf("duplicate module name: %s", sub.name))
} }
unique[sub.name] = true unique[sub.name] = true
} }
return &Lifecycle{ return &Lifecycle{
logger: defaultLogger, logger: defaultLogger,
subsystems: subsystems, modules: modules,
setupTracker: make(map[string]bool), setupTracker: make(map[string]bool),
teardownTracker: make(map[string]bool), teardownTracker: make(map[string]bool),
} }
@@ -61,13 +67,21 @@ func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle {
return app return app
} }
// Setup initializes all sub-systems in the order they were defined and checks // Logger returns the logger for the lifecycle.
// for dependencies. It returns an error if any sub-system fails to initialize or 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 // 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. // of the application lifecycle to ensure all resources are cleaned up properly.
func (app *Lifecycle) Setup() error { func (app *Lifecycle) Setup() error {
setupCount := 0 setupCount := 0
for _, sub := range app.subsystems { for _, sub := range app.modules {
if ok, err := app.setupSingle(sub, app.logger); err != nil { if ok, err := app.setupSingle(sub, app.logger); err != nil {
return err return err
} else if ok { } else if ok {
@@ -75,76 +89,76 @@ func (app *Lifecycle) Setup() error {
} }
} }
app.logger.Info("Lifecycle sub-systems initialized, backend setup complete", app.logger.Info("Lifecycle modules initialized, backend setup complete",
"count", setupCount, "subsystems", mapToString(app.setupTracker)) "count", setupCount, "modules", mapToString(app.setupTracker))
return nil return nil
} }
// Teardown runs all sub-system teardown functions in reverse order of setup. // Teardown runs all module teardown functions in reverse order of setup.
// Teardown should always be run at the end of the application lifecycle // 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.
func (app *Lifecycle) Teardown() error { func (app *Lifecycle) Teardown() error {
teardownCount := 0 teardownCount := 0
for i := len(app.subsystems) - 1; i >= 0; i-- { for i := len(app.modules) - 1; i >= 0; i-- {
if ok, err := app.singleTeardown(app.subsystems[i]); err != nil { if ok, err := app.singleTeardown(app.modules[i]); err != nil {
return err return err
} else if ok { } else if ok {
teardownCount++ teardownCount++
} }
} }
app.logger.Info("All sub-systems torn down, backend teardown complete", app.logger.Info("All modules torn down, backend teardown complete",
"count", teardownCount, "subsystems", mapToString(app.teardownTracker)) "count", teardownCount, "modules", mapToString(app.teardownTracker))
return nil return nil
} }
// Require adds sub-system(s) to the lifecycle and immediately runs any setup // Require adds module(s) to the lifecycle and immediately runs any setup
// functions. Relevant when a sub-system is not part of the main application // functions. Relevant when a module is not part of the main application
// but may still be conditionally necessary. Any sub-systems that are already // but may still be conditionally necessary. Any modules that are already
// set up are ignored. // set up are ignored.
func (app *Lifecycle) Require(subsystems ...*Subsystem) error { func (app *Lifecycle) Require(modules ...*Module) error {
return app.RequireL(app.logger, subsystems...) return app.RequireL(app.logger, modules...)
} }
// RequireL adds sub-system(s) to the lifecycle with a specific logger and // RequireL adds module(s) to the lifecycle with a specific logger and
// immediately runs any setup functions. See Require for more details. // immediately runs any setup functions. See Require for more details.
// This variation is useful when you need to set up sub-systems with a non- // This variation is useful when you need to set up modules with a non-
// default logger. // default logger.
func (app *Lifecycle) RequireL(logger *slog.Logger, subsystems ...*Subsystem) error { func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
for _, sub := range subsystems { for _, sub := range modules {
if sub == nil { if sub == nil {
return fmt.Errorf("sub-system is nil") return fmt.Errorf("module is nil")
} }
// Check if the sub-system is already set up // Check if the module is already set up
if _, ok := app.setupTracker[sub.name]; ok { if _, ok := app.setupTracker[sub.name]; ok {
app.logger.Warn("sub-system already set up, ignoring", "subsystem", sub.name) app.logger.Warn("module already set up, ignoring", "module", sub.name)
continue continue
} }
// Add the sub-system to the lifecycle // Add the module to the lifecycle
app.subsystems = append(app.subsystems, sub) app.modules = append(app.modules, sub)
// Run the setup function for the sub-system // Run the setup function for the module
if _, err := app.setupSingle(sub, logger); err != nil { if _, err := app.setupSingle(sub, logger); err != nil {
return fmt.Errorf("error setting up required subsystem '%s': %w", sub.name, err) return fmt.Errorf("error setting up required module '%s': %w", sub.name, err)
} }
} }
app.logger.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker))
return nil return nil
} }
// setupSingle is a helper function to set up a single sub-system. // setupSingle is a helper function to set up a single module.
func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, error) { func (app *Lifecycle) setupSingle(sub *Module, logger *slog.Logger) (bool, error) {
if sub == nil { if sub == nil {
return false, fmt.Errorf("sub-system is nil") return false, fmt.Errorf("module is nil")
} }
// Set the logger for the sub-system // Set the logger for the module
sub.logger = logger sub.logger = logger
// Check if all dependencies are satisfied // Check if all dependencies are satisfied
@@ -163,12 +177,12 @@ func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, er
} }
if sub.setup != nil { if sub.setup != nil {
// Run the setup function for the sub-system // Run the setup function for the module
if err := sub.setup(); err != nil { if err := sub.setup(); err != nil {
return false, fmt.Errorf("error initializing '%s': %w", sub.name, err) return false, fmt.Errorf("error initializing '%s': %w", sub.name, err)
} }
// Mark this subsystem as setup // Mark this module as setup
app.setupTracker[sub.name] = true app.setupTracker[sub.name] = true
return true, nil return true, nil
} }
@@ -176,22 +190,22 @@ func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, er
return false, nil return false, nil
} }
// singleTeardown is a helper function to tear down a single sub-system. // singleTeardown is a helper function to tear down a single module.
func (app *Lifecycle) singleTeardown(sub *Subsystem) (bool, error) { func (app *Lifecycle) singleTeardown(sub *Module) (bool, error) {
if sub == nil { if sub == nil {
return false, fmt.Errorf("sub-system is nil") return false, fmt.Errorf("module is nil")
} }
// Check if the sub-system was set up // Check if the module was set up
if _, ok := app.setupTracker[sub.name]; !ok { if _, ok := app.setupTracker[sub.name]; !ok {
return false, nil return false, nil
} }
// Run the teardown function for the sub-system // Run the teardown function for the module
if sub.teardown != nil { if sub.teardown != nil {
sub.teardown() sub.teardown()
// Mark this subsystem as torn down // Mark this module as torn down
app.teardownTracker[sub.name] = true app.teardownTracker[sub.name] = true
return true, nil return true, nil
} }

49
module.go Normal file
View File

@@ -0,0 +1,49 @@
package app
import (
"fmt"
"log/slog"
)
// Module represents a sub-system of the application, with its setup and
// teardown functions and dependencies.
type Module struct {
logger *slog.Logger
name string
setup setupFn
teardown teardownFn
depends []*Module
}
// ModuleOpts contains user-exposed options when defining a module.
type ModuleOpts struct {
// Setup is the setup function for the module.
Setup setupFn
// Teardown is the teardown function for the module.
Teardown teardownFn
// Depends is a list of modules that this module depends on.
Depends []*Module
}
// NewModule creates a new Module instance with the given name and options.
func NewModule(name string, opts ModuleOpts) *Module {
return &Module{
name: name,
setup: opts.Setup,
teardown: opts.Teardown,
depends: opts.Depends,
}
}
// Logger returns the logger for the module.
func (s *Module) Logger() *slog.Logger {
if s.logger == nil {
panic(fmt.Sprintf("subsytem %s used before logger was initialized", s.name))
}
return s.logger
}
// Name returns the name of the module.
func (s *Module) Name() string {
return s.name
}

View File

@@ -1,44 +0,0 @@
package app
import (
"fmt"
"log/slog"
)
// Subsystem represents a sub-system of the application, with its setup and
// teardown functions and dependencies.
type Subsystem struct {
logger *slog.Logger
name string
setup setupFn
teardown teardownFn
depends []*Subsystem
}
// SubsystemOpts contains user-exposed options when defining a subsystem.
type SubsystemOpts struct {
// Setup is the setup function for the subsystem.
Setup setupFn
// Teardown is the teardown function for the subsystem.
Teardown teardownFn
// Depends is a list of subsystems that this subsystem depends on.
Depends []*Subsystem
}
// NewSubsystem creates a new Subsystem instance with the given name and options.
func NewSubsystem(name string, opts SubsystemOpts) *Subsystem {
return &Subsystem{
name: name,
setup: opts.Setup,
teardown: opts.Teardown,
depends: opts.Depends,
}
}
// Logger returns the logger for the subsystem.
func (s *Subsystem) Logger() *slog.Logger {
if s.logger == nil {
panic(fmt.Sprintf("subsytem %s used before logger was initialized", s.name))
}
return s.logger
}