From f43924763122a94c12248e9c35ea0f04e01daf58 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Fri, 30 May 2025 10:47:33 -0700 Subject: [PATCH] initial commit --- README.md | 3 + app.go | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + 3 files changed, 233 insertions(+) create mode 100644 README.md create mode 100644 app.go create mode 100644 go.mod diff --git a/README.md b/README.md new file mode 100644 index 0000000..cff875a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# app + +app is a simple and lightweight app lifecycle management library. diff --git a/app.go b/app.go new file mode 100644 index 0000000..0e66963 --- /dev/null +++ b/app.go @@ -0,0 +1,227 @@ +// Package app provides the core setup and teardown functions for the +// backend application. It handles the initialization of all sub-systems +// 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/go.mod b/go.mod new file mode 100644 index 0000000..cd763c5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.auvem.com/go-toolkit/app + +go 1.24.0