fix: correct lifecycle setup and teardown semantics

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>
This commit is contained in:
2026-06-29 17:58:00 -07:00
parent b7adafed67
commit 4cd218f658
4 changed files with 287 additions and 204 deletions
+170
View File
@@ -0,0 +1,170 @@
package app
import (
"fmt"
"log/slog"
"sort"
"strings"
)
func (app *Lifecycle) require(opts RequireOpts, modules ...*Module) error {
if len(modules) == 0 {
return fmt.Errorf("no modules to require")
}
if opts.Logger != nil && opts.Unique {
// unique with custom logger is valid
}
if opts.Logger == nil && opts.Unique {
// valid
}
setupBefore := len(app.setupOrder)
for i, mod := range modules {
if mod == nil {
app.rollbackFrom(setupBefore)
return fmt.Errorf("module %d is nil", i)
}
if _, ok := app.setupTracker[mod.name]; ok {
if opts.Unique {
app.rollbackFrom(setupBefore)
return fmt.Errorf("module %s is already set up, cannot require again", mod)
}
app.Logger().Warn("module already set up, ignoring", "module", mod)
mod.loaded = true
mod.lifecycle = app
mod.logger = opts.Logger
continue
}
app.modules = append(app.modules, mod)
if err := app.setupSingle(opts.Logger, mod, nil); err != nil {
app.rollbackFrom(setupBefore)
return fmt.Errorf("error setting up required module %s: %w", mod, err)
}
}
app.Logger().Info("New modules initialized", "all", mapToString(app.setupTracker))
return nil
}
func (app *Lifecycle) setupSingle(logger *slog.Logger, mod *Module, visiting map[string]bool) error {
if mod == nil {
return fmt.Errorf("module is nil")
}
if _, ok := app.setupTracker[mod.name]; ok {
return nil
}
if visiting == nil {
visiting = make(map[string]bool)
}
if visiting[mod.name] {
return fmt.Errorf("circular dependency detected involving %s", mod)
}
visiting[mod.name] = true
defer delete(visiting, mod.name)
mod.lifecycle = app
mod.logger = logger
for _, dep := range mod.depends {
if _, ok := app.setupTracker[dep]; !ok {
if app.opts.DisableAutoload {
return fmt.Errorf("dependency %s not satisfied for '%s'", dep, mod)
}
depmod, err := app.getModuleByName(dep)
if err != nil {
return fmt.Errorf("error getting dependency '%s' for %s: %w", dep, mod, err)
}
if err := app.setupSingle(logger, depmod, visiting); err != nil {
return fmt.Errorf("error setting up dependency %s for %s: %w", depmod, mod, err)
}
}
}
if mod.setup != nil {
if err := mod.setup(mod); err != nil {
return fmt.Errorf("error initializing %s: %w", mod, err)
}
}
app.setupTracker[mod.name] = app.setupCount
app.setupOrder = append(app.setupOrder, mod)
app.setupCount++
mod.loaded = true
return nil
}
func (app *Lifecycle) teardownSingle(mod *Module) error {
if mod == nil {
return fmt.Errorf("module is nil")
}
if _, ok := app.setupTracker[mod.name]; !ok {
return fmt.Errorf("module %s is not set up, cannot tear down", mod)
}
if mod.teardown != nil {
if err := mod.teardown(mod); err != nil {
return fmt.Errorf("error tearing down %s: %w", mod, err)
}
}
app.teardownTracker[mod.name] = app.teardownCount
app.teardownCount++
mod.loaded = false
return nil
}
func (app *Lifecycle) rollbackFrom(startIndex int) {
for i := len(app.setupOrder) - 1; i >= startIndex; i-- {
mod := app.setupOrder[i]
if mod.teardown != nil {
_ = mod.teardown(mod)
}
delete(app.setupTracker, mod.name)
mod.loaded = false
}
app.setupOrder = app.setupOrder[:startIndex]
app.setupCount = startIndex
}
func (app *Lifecycle) getModuleByName(name string) (*Module, error) {
for _, mod := range app.modules {
if mod.name == name {
return mod, nil
}
}
return nil, ErrModuleNotFound
}
func mapToString(m map[string]int) string {
if len(m) == 0 {
return "[]"
}
values := make([]int, 0, len(m))
reverseMap := make(map[int]string, len(m))
for k, v := range m {
values = append(values, v)
reverseMap[v] = k
}
sort.Ints(values)
result := make([]string, 0, len(m))
for _, v := range values {
if name, ok := reverseMap[v]; ok {
result = append(result, fmt.Sprintf("'%s'", name))
}
}
return "[" + strings.Join(result, " ") + "]"
}