4cd218f658
Guard against double setup when autoload runs dependencies early, tear down in reverse setup order, roll back on partial failure, and detect circular Depends chains. Co-authored-by: Cursor <cursoragent@cursor.com>
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const lifecycleContextKey contextKey = "lifecycle"
|
|
|
|
// LifecycleOpts contains user-exposed options when defining a lifecycle.
|
|
type LifecycleOpts struct {
|
|
// DisableAutoload disables dependency autoloading (enabled by default).
|
|
DisableAutoload bool
|
|
|
|
// Logger is the logger for the lifecycle. If not set, it will be initialized
|
|
// by the lifecycle using the default logger (slog.Default).
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// Lifecycle represents the core application structure. Lifecycle manages
|
|
// resources providing an orchestrator for setup and teardown of modules.
|
|
type Lifecycle struct {
|
|
modules []*Module
|
|
opts LifecycleOpts
|
|
|
|
setupOrder []*Module
|
|
setupCount int
|
|
setupTracker map[string]int
|
|
teardownCount int
|
|
teardownTracker map[string]int
|
|
}
|
|
|
|
// 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 {
|
|
unique := make(map[string]bool)
|
|
for _, mod := range modules {
|
|
if _, exists := unique[mod.name]; exists {
|
|
panic(fmt.Sprintf("duplicate module: %s", mod))
|
|
}
|
|
unique[mod.name] = true
|
|
}
|
|
|
|
return &Lifecycle{
|
|
modules: modules,
|
|
setupTracker: make(map[string]int),
|
|
teardownTracker: make(map[string]int),
|
|
}
|
|
}
|
|
|
|
// LifecycleFromContext retrieves the Lifecycle from the context. Returns nil if not found.
|
|
func LifecycleFromContext(ctx context.Context) *Lifecycle {
|
|
if lifecycle, ok := ctx.Value(lifecycleContextKey).(*Lifecycle); ok {
|
|
return lifecycle
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Context adds the Lifecycle to a context and returns the new context.
|
|
func (app *Lifecycle) Context(ctx context.Context) context.Context {
|
|
if app == nil {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, lifecycleContextKey, app)
|
|
}
|
|
|
|
// WithOpts sets the options for the lifecycle.
|
|
func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle {
|
|
app.opts = opts
|
|
return app
|
|
}
|
|
|
|
// WithLogger sets the logger for the lifecycle. Panics if the logger is nil.
|
|
func (app *Lifecycle) WithLogger(logger *slog.Logger) *Lifecycle {
|
|
if logger == nil {
|
|
panic("logger cannot be nil")
|
|
}
|
|
app.opts.Logger = logger
|
|
return app
|
|
}
|
|
|
|
// Logger returns the logger for the lifecycle.
|
|
func (app *Lifecycle) Logger() *slog.Logger {
|
|
if app == nil {
|
|
panic("lifecycle is nil, cannot get logger")
|
|
}
|
|
if app.opts.Logger == nil {
|
|
app.opts.Logger = slog.Default()
|
|
}
|
|
return app.opts.Logger
|
|
}
|
|
|
|
// Setup initializes all registered modules, resolving dependencies via autoload
|
|
// when enabled. Modules run setup in dependency order; among independent modules,
|
|
// registration order is preserved. Teardown runs in reverse setup order.
|
|
func (app *Lifecycle) Setup() error {
|
|
if app.setupCount > 0 {
|
|
return fmt.Errorf("lifecycle already set up, cannot set up again")
|
|
}
|
|
|
|
setupBefore := len(app.setupOrder)
|
|
for _, mod := range app.modules {
|
|
if err := app.setupSingle(nil, mod, nil); err != nil {
|
|
app.rollbackFrom(setupBefore)
|
|
return err
|
|
}
|
|
}
|
|
|
|
app.Logger().Info("Lifecycle modules initialized, backend setup complete",
|
|
"modules", mapToString(app.setupTracker))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Teardown runs teardown for all set-up modules in reverse setup order.
|
|
// All module teardown errors are joined and returned (non-blocking).
|
|
func (app *Lifecycle) Teardown() error {
|
|
if app.teardownCount > 0 {
|
|
return fmt.Errorf("lifecycle already torn down, cannot tear down again")
|
|
}
|
|
|
|
var err error
|
|
var failureCount int
|
|
for i := len(app.setupOrder) - 1; i >= 0; i-- {
|
|
if singleErr := app.teardownSingle(app.setupOrder[i]); singleErr != nil {
|
|
err = errors.Join(err, singleErr)
|
|
failureCount++
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
app.Logger().Error("Error tearing down modules", "failures", failureCount, "error", err)
|
|
return err
|
|
}
|
|
|
|
app.Logger().Info("All modules torn down, backend teardown complete",
|
|
"modules", mapToString(app.teardownTracker))
|
|
|
|
return nil
|
|
}
|