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>
171 lines
3.8 KiB
Go
171 lines
3.8 KiB
Go
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, " ") + "]"
|
|
}
|