split into separate files
This commit is contained in:
222
app.go
222
app.go
@@ -3,225 +3,3 @@
|
|||||||
// and the logger. app should never be imported except by a main package.
|
// and the logger. app should never be imported except by a main package.
|
||||||
// Examples of suitable main packages are cmd and test.
|
// Examples of suitable main packages are cmd and test.
|
||||||
package app
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
194
lifecycle.go
Normal file
194
lifecycle.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
30
subsystem.go
Normal file
30
subsystem.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user