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:
@@ -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, " ") + "]"
|
||||
}
|
||||
Reference in New Issue
Block a user