From 2f702cccd40a69bb0fbc2e05c50c70ca5b5e9320 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Fri, 30 May 2025 11:03:44 -0700 Subject: [PATCH] split into separate files --- app.go | 222 --------------------------------------------------- lifecycle.go | 194 ++++++++++++++++++++++++++++++++++++++++++++ subsystem.go | 30 +++++++ 3 files changed, 224 insertions(+), 222 deletions(-) create mode 100644 lifecycle.go create mode 100644 subsystem.go diff --git a/app.go b/app.go index 0e66963..23080fd 100644 --- a/app.go +++ b/app.go @@ -3,225 +3,3 @@ // and the logger. app should never be imported except by a main package. // Examples of suitable main packages are cmd and test. package app - -import ( - "fmt" - "log/slog" -) - -// setupFn is a function that runs setup logic for a sub-system. -type setupFn func() error - -// teardownFn is a function that runs teardown logic for a sub-system. -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 sub-systems. -type Lifecycle struct { - subsystems []*Subsystem - setupTracker map[string]bool - teardownTracker map[string]bool - opts LifecycleOpts -} - -// NewLifecycle creates a new Lifecycle instance with the given subsystems. -func NewLifecycle(subsystems ...*Subsystem) *Lifecycle { - // Ensure subsystems are unique - unique := make(map[string]bool) - for _, sub := range subsystems { - if _, exists := unique[sub.name]; exists { - panic(fmt.Sprintf("duplicate subsystem name: %s", sub.name)) - } - unique[sub.name] = true - } - - return &Lifecycle{ - subsystems: subsystems, - 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 -} - -// Setup initializes all sub-systems in the order they were defined and checks -// for dependencies. It returns an error if any sub-system 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.subsystems { - if ok, err := app.setupSingle(sub); err != nil { - return err - } else if ok { - setupCount++ - } - } - - slog.Info("Lifecycle sub-systems initialized, backend setup complete", - "count", setupCount, "subsystems", mapToString(app.setupTracker)) - - return nil -} - -// Teardown runs all sub-system 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.subsystems) - 1; i >= 0; i-- { - if ok, err := app.singleTeardown(app.subsystems[i]); err != nil { - return err - } else if ok { - teardownCount++ - } - } - - slog.Info("All sub-systems torn down, backend teardown complete", - "count", teardownCount, "subsystems", mapToString(app.teardownTracker)) - - return nil -} - -// Require adds sub-system(s) to the lifecycle and immediately runs any setup -// function. Relevant when a sub-system is not part of the main application -// but may still be conditionally necessary. Any sub-systems that are already -// set up are ignored. -func (app *Lifecycle) Require(subsystems ...*Subsystem) error { - for _, sub := range subsystems { - if sub == nil { - return fmt.Errorf("sub-system is nil") - } - - // Check if the sub-system is already set up - if _, ok := app.setupTracker[sub.name]; ok { - slog.Warn("sub-system already set up, ignoring", "subsystem", sub.name) - continue - } - - // Add the sub-system to the lifecycle - app.subsystems = append(app.subsystems, sub) - - // Run the setup function for the sub-system - if _, err := app.setupSingle(sub); err != nil { - return fmt.Errorf("error setting up required subsystem '%s': %w", sub.name, err) - } - } - - slog.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) - - return nil -} - -// setupSingle is a helper function to set up a single sub-system. -func (app *Lifecycle) setupSingle(sub *Subsystem) (bool, error) { - if sub == nil { - return false, fmt.Errorf("sub-system is nil") - } - - // Check if all dependencies are satisfied - for _, dep := range sub.depends { - if _, ok := app.setupTracker[dep.name]; !ok { - if app.opts.DisableAutoload { - return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep.name, sub.name) - } else { - // Attempt to set up the dependency - if _, err := app.setupSingle(dep); err != nil { - return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep.name, sub.name, err) - } - - } - } - } - - if sub.setup != nil { - // Run the setup function for the sub-system - if err := sub.setup(); err != nil { - return false, fmt.Errorf("error initializing '%s': %w", sub.name, err) - } - - // Mark this subsystem as setup - app.setupTracker[sub.name] = true - return true, nil - } - - return false, nil -} - -// singleTeardown is a helper function to tear down a single sub-system. -func (app *Lifecycle) singleTeardown(sub *Subsystem) (bool, error) { - if sub == nil { - return false, fmt.Errorf("sub-system is nil") - } - - // Check if the sub-system was set up - if _, ok := app.setupTracker[sub.name]; !ok { - return false, nil - } - - // Run the teardown function for the sub-system - if sub.teardown != nil { - sub.teardown() - - // Mark this subsystem as torn down - app.teardownTracker[sub.name] = true - return true, nil - } - - return false, nil -} - -// Subsystem represents a sub-system of the application, with its setup and -// teardown functions and dependencies. -type Subsystem struct { - 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, - } -} - -// 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 -} diff --git a/lifecycle.go b/lifecycle.go new file mode 100644 index 0000000..5c759f8 --- /dev/null +++ b/lifecycle.go @@ -0,0 +1,194 @@ +package app + +import ( + "fmt" + "log/slog" +) + +// setupFn is a function that runs setup logic for a sub-system. +type setupFn func() error + +// teardownFn is a function that runs teardown logic for a sub-system. +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 sub-systems. +type Lifecycle struct { + subsystems []*Subsystem + setupTracker map[string]bool + teardownTracker map[string]bool + opts LifecycleOpts +} + +// NewLifecycle creates a new Lifecycle instance with the given subsystems. +func NewLifecycle(subsystems ...*Subsystem) *Lifecycle { + // Ensure subsystems are unique + unique := make(map[string]bool) + for _, sub := range subsystems { + if _, exists := unique[sub.name]; exists { + panic(fmt.Sprintf("duplicate subsystem name: %s", sub.name)) + } + unique[sub.name] = true + } + + return &Lifecycle{ + subsystems: subsystems, + 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 +} + +// Setup initializes all sub-systems in the order they were defined and checks +// for dependencies. It returns an error if any sub-system 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.subsystems { + if ok, err := app.setupSingle(sub); err != nil { + return err + } else if ok { + setupCount++ + } + } + + slog.Info("Lifecycle sub-systems initialized, backend setup complete", + "count", setupCount, "subsystems", mapToString(app.setupTracker)) + + return nil +} + +// Teardown runs all sub-system 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.subsystems) - 1; i >= 0; i-- { + if ok, err := app.singleTeardown(app.subsystems[i]); err != nil { + return err + } else if ok { + teardownCount++ + } + } + + slog.Info("All sub-systems torn down, backend teardown complete", + "count", teardownCount, "subsystems", mapToString(app.teardownTracker)) + + return nil +} + +// Require adds sub-system(s) to the lifecycle and immediately runs any setup +// function. Relevant when a sub-system is not part of the main application +// but may still be conditionally necessary. Any sub-systems that are already +// set up are ignored. +func (app *Lifecycle) Require(subsystems ...*Subsystem) error { + for _, sub := range subsystems { + if sub == nil { + return fmt.Errorf("sub-system is nil") + } + + // Check if the sub-system is already set up + if _, ok := app.setupTracker[sub.name]; ok { + slog.Warn("sub-system already set up, ignoring", "subsystem", sub.name) + continue + } + + // Add the sub-system to the lifecycle + app.subsystems = append(app.subsystems, sub) + + // Run the setup function for the sub-system + if _, err := app.setupSingle(sub); err != nil { + return fmt.Errorf("error setting up required subsystem '%s': %w", sub.name, err) + } + } + + slog.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) + + return nil +} + +// setupSingle is a helper function to set up a single sub-system. +func (app *Lifecycle) setupSingle(sub *Subsystem) (bool, error) { + if sub == nil { + return false, fmt.Errorf("sub-system is nil") + } + + // Check if all dependencies are satisfied + for _, dep := range sub.depends { + if _, ok := app.setupTracker[dep.name]; !ok { + if app.opts.DisableAutoload { + return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep.name, sub.name) + } else { + // Attempt to set up the dependency + if _, err := app.setupSingle(dep); err != nil { + return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep.name, sub.name, err) + } + + } + } + } + + if sub.setup != nil { + // Run the setup function for the sub-system + if err := sub.setup(); err != nil { + return false, fmt.Errorf("error initializing '%s': %w", sub.name, err) + } + + // Mark this subsystem as setup + app.setupTracker[sub.name] = true + return true, nil + } + + return false, nil +} + +// singleTeardown is a helper function to tear down a single sub-system. +func (app *Lifecycle) singleTeardown(sub *Subsystem) (bool, error) { + if sub == nil { + return false, fmt.Errorf("sub-system is nil") + } + + // Check if the sub-system was set up + if _, ok := app.setupTracker[sub.name]; !ok { + return false, nil + } + + // Run the teardown function for the sub-system + if sub.teardown != nil { + sub.teardown() + + // Mark this subsystem as torn down + app.teardownTracker[sub.name] = true + return true, nil + } + + return false, nil +} + +// 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 +} diff --git a/subsystem.go b/subsystem.go new file mode 100644 index 0000000..da72851 --- /dev/null +++ b/subsystem.go @@ -0,0 +1,30 @@ +package app + +// Subsystem represents a sub-system of the application, with its setup and +// teardown functions and dependencies. +type Subsystem struct { + 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, + } +}