diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e46e4..eda18b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ below says plainly whether an upgrade can break a caller. ## [Unreleased] +No signature changes. An upgrade cannot break a caller. + +### Fixed + +- A panicking `OnStart` hook reports its `EventStart`, with the panic as + `Err`, the way a panicking `OnDrain` or `OnStop` hook already reported + theirs. Observers saw no start step at all for such a service; the failure + still reached `Start` and `Resolve` as before. + ## [0.15.0] - 2026-09-10 An application can say what it is doing. `go doc -all` against 0.14.0 adds one diff --git a/CLAUDE.md b/CLAUDE.md index d27b578..fadba77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co `github.com/floatdrop/di` is a dependency-injection container for Go 1.27+ built on generic methods. [`docs/DESIGN.md`](docs/DESIGN.md) explains resolution, lifetimes, phases, cycles and teardown with diagrams; this file is the working -detail behind it, and the two are edited together. The library is `di.go`, the -rendering of the recorded graph in `explain.go`, the check of the declared -graph in `validate.go`, the net/http adapter in `dihttp/` and the slog bridge -for `Observe` in `dislog/`; everything else is tests and two separate modules, -`examples/` and `benchmarks/`. +detail behind it, and the two are edited together. The library is six files: +`di.go` (package doc, keys, events, `Scope`, modules, `Test`), `binding.go` +(registration and the `Binding` handle), `state.go` (a scope's registry, +`freeze`, the parent-chain readers), `resolve.go` (the resolution path, both +cycle detectors, the build step, `Get` and friends), `lifecycle.go` (the +instance phase machine, hooks, `Start` and `Stop`) and `run.go` (`Run` and +`Shutdown`). Beside them are the rendering of the recorded graph in +`explain.go`, the check of the declared graph in `validate.go`, the net/http +adapter in `dihttp/` and the slog bridge for `Observe` in `dislog/`; +everything else is tests and two separate modules, `examples/` and +`benchmarks/`. ## Commands @@ -44,8 +50,8 @@ test -z "$(gofmt -l .)" && go vet ./... && go test -race -count=1 ./... \ ## Architecture -Reading `di.go` top to bottom does not reveal the model; these are the pieces -that only make sense together. +Reading the library top to bottom does not reveal the model; these are the +pieces that only make sense together. **Three levels of state.** A `binding` is a registration: its key, lifetime, hooks, and `build` func. An `instance` is one built value of a binding. A @@ -297,7 +303,7 @@ prototyped beside it and dropped: fourteen methods to save a repeated type argument, with the compiler checking only an arity the reflective form cannot get wrong.) Every registration method calls `register` directly, because `callsite` counts a fixed number of frames and a `Wire` that went through -`Provide` would record a site inside `di.go`. +`Provide` would record a site inside the library. `Explain` draws `wants` under an unbuilt node with dashed edges (`declaredInto`), switching back to the recorded tree wherever a declared @@ -389,10 +395,12 @@ harness classify panics by that rule. **When a stop is owed.** `OnStop` runs when `OnStart` succeeded, or when there is no `OnStart` to pair with (or the scope was never started), making `OnStop` a plain destructor. A service built but never started is *not* torn down. Only a -start hook that *returned* counts as succeeded: `startClaimed` recovers a -panicking hook into a failed start, or the instance would sit at -`phaseStarted`, be served to a caller that recovered the panic, and be paired -with an `OnStop` for an `OnStart` that never finished. `binding.used` is set +start hook that *returned* counts as succeeded: `callHook` turns a panicking +hook into a failed start, or the instance would sit at `phaseStarted`, be +served to a caller that recovered the panic, and be paired with an `OnStop` +for an `OnStart` that never finished. `instance.paired` and `instance.owes` +are the one statement of that predicate, shared by the drain and stop steps. +`binding.used` is set only when a resolution actually served a value, and `state.served` records keys a scope resolved from an *outer* scope, because `used` lives on the outer binding and cannot protect the inner scope. @@ -413,8 +421,9 @@ binding and cannot protect the inner scope. given gets an error naming `Shutdown`. A hook that passes a context of its own is invisible and waits, which is why the fallback still has to be a bounded wait rather than a promise. -- Every user hook is called through `callHook` (or `startClaimed`'s - equivalent), which turns a panic into that hook's error. A cancelled +- Every user hook is called through `callHook`, which turns a panic into that + hook's error, and every step is reported through `state.report`, so a + hook that panicked is observed like one that failed. A cancelled `Worker`'s return is dropped only when it says nothing beyond `context.Canceled` (`onlyCancellation` walks the error tree); `errors.Is` matched `errors.Join(ctx.Err(), failure)` and dropped the failure with it @@ -446,7 +455,7 @@ Provenance is a tag on each test -- `(review 2, 5)`, `(pass 4)` -- because grouping by it put three files between two tests of the same machine; each file's header explains the tags and the commit each review was checked against. **Verify a new test fails against the commit that preceded the fix**, e.g. by -restoring the old `di.go` from git and running just that test, and tag it. +restoring the old library files from git and running just that test, and tag it. Several tests here turned out to pass both before and after; say so rather than implying coverage. @@ -534,7 +543,8 @@ each one as it arrives leaves the seed nothing to decide. `TestMachineConcurrentShapes` builds op sequences directly rather than from bytes, because a byte seed has to survive four modulos to reach a particular interleaving; its three shapes are what the coverage gap said no random -sequence was reaching. Delete either deferred release in `di.go` and C9 fails. +sequence was reaching. Delete either deferred release in `lifecycle.go` and C9 +fails. `FuzzMachine` and `FuzzMachineConcurrent` run the same invariants under coverage-guided search; the corpus in `testdata/fuzz/` is committed and CI runs @@ -556,9 +566,9 @@ which is the map of where the next review will dig: every defect the September 2026 reviews found lived on such a line. CI runs it with a floor of 90% generator coverage; when the floor moves, move it up. CI also checks the script's arithmetic against `go tool cover -func`, because it has been wrong -twice -- once keying coverage blocks by line number, when `di.go` has eighteen -lines carrying more than one block, and once attributing a block to a function -in the wrong file when `explain.go` arrived. Both answers looked plausible, +twice -- once keying coverage blocks by line number, when the library has +eighteen lines carrying more than one block, and once attributing a block to a +function in the wrong file when `explain.go` arrived. Both answers looked plausible, which is the dangerous kind of wrong. A tool that measures a gap has to be measured itself. diff --git a/binding.go b/binding.go new file mode 100644 index 0000000..02eb351 --- /dev/null +++ b/binding.go @@ -0,0 +1,396 @@ +package di + +// Registration: what a binding is, the methods that make one, and the typed +// handle that refines it. Nothing here builds anything; a binding's build +// func is called by the resolution in resolve.go. + +import ( + "context" + "fmt" + "reflect" + "runtime" + "slices" + "sync/atomic" +) + +// binding is one registration: its key, lifetime, hooks and build func. +type binding struct { + key key + site string + module string // the Module this was registered from, or "" + group bool + scoped bool + eager bool + override bool // declared to replace an earlier registration of the key + isValue bool // registered with Value: lifetimes do not apply + wants []key // the parameter types of a Wire constructor; nil for a Provide closure + build func(*Scope) any + + // inner is the registration a Wrap composes over, bound when Wrap is + // called, and innerAt the scope that registered it; both nil for any + // other binding. wrappedBy is set on a binding a Wrap has bound to: an + // Override that replaced it would leave the wrapper composing over a + // registration that no longer serves the key. + inner *binding + innerAt *state + wrappedBy atomic.Pointer[binding] + onStart func(context.Context, any) error + onDrain func(context.Context, any) error + onStop func(context.Context, any) error + worker func(context.Context, any) error + + // used is set once this binding has served a value. From then on the + // registration cannot be overridden, since that would leave two live + // instances of one service. A failed resolution built nothing and leaves + // the key re-registerable; that is how a key whose constructor failed is + // recovered. + used atomic.Bool + + // resolving counts the resolutions of this binding that have not served + // a value yet, the window used cannot cover: a constructor that registers + // over its own key and resolves the replacement would otherwise hand the + // nested call the new value and the outer call the old one. It is read + // only until the first value is served; after that used says the same. + resolving atomic.Int32 + + single *instance // the singleton; scoped bindings keep one instance per state +} + +// where names the registration for a message: its site, and the module it was +// registered from when there is one, as in "storage (wire.go:12)". +func (b *binding) where() string { + if b.module == "" { + return b.site + } + return b.module + " (" + b.site + ")" +} + +// validate rejects lifetime and hook combinations that cannot be honoured. +// It runs at freeze, so the order the builder methods were called in does +// not matter. +func (b *binding) validate() { + bad := func(what, why string) { + panic(fmt.Sprintf("di: %s (provided at %s): %s %s", b.key, b.where(), what, why)) + } + switch { + case b.eager && b.scoped: + // Rejected even if a later registration overrides it. Whether an + // override inherits eagerness is decided in deriveEager. + bad("Eager", "does not apply to a Scoped binding: it is not built once") + case b.isValue && b.scoped: + bad("Scoped", "is meaningless for a Value binding: the instance already exists") + case b.group && b.override: + bad("Override", "does not apply to a group member: members accumulate rather than replace one another") + case b.inner != nil && b.group: + bad("Group", "does not apply to a wrapper: it serves the key it wraps") + case b.inner != nil && b.override: + bad("Override", "does not apply to a wrapper: it composes over the registration it wraps rather than replacing it") + } +} + +// Binding is the typed handle returned by Provide, Value, Wire and Wrap. Its +// methods refine the registration; they must be called before the first +// resolution from this scope. +type Binding[T any] struct { + s *Scope + b *binding +} + +func (s *Scope) register(k key, build func(*Scope) any) *binding { + b := &binding{key: k, site: callsite(), module: s.module, build: build} + b.single = &instance{b: b} + s.mu.Lock() + s.pending = append(s.pending, b) + s.mu.Unlock() + return b +} + +// Provide registers a lazily built singleton. T is inferred from the +// constructor's return type; dependencies are pulled with s.Get[...](). +func (s *Scope) Provide[T any](ctor func(*Scope) T) Binding[T] { + return Binding[T]{s, s.register(key{t: reflect.TypeFor[T]()}, func(s *Scope) any { return ctor(s) })} +} + +// Value registers an already-built instance. +func (s *Scope) Value[T any](v T) Binding[T] { + b := s.register(key{t: reflect.TypeFor[T]()}, func(*Scope) any { return v }) + b.isValue = true + return Binding[T]{s, b} +} + +func callsite() string { + _, file, line, _ := runtime.Caller(3) + return fmt.Sprintf("%s:%d", file, line) +} + +// Wire registers a lazily built singleton from a constructor of any arity, +// whose parameters are its dependencies: +// +// s.Wire[*Server](NewServer) // func NewServer(cfg Config, repo *Repo) *Server +// +// ctor must be a non-variadic function returning T, or T and an error, and is +// read with reflection once, here. Each parameter type is resolved from the +// same scope view a Provide closure would see, so lifetimes, cycles, hooks and +// error paths are unchanged; what Wire adds is that the dependencies are known +// at registration, before anything is built. A non-nil error from ctor aborts +// the build exactly as s.Must does. +// +// T cannot be inferred from an untyped argument, so it is spelled out, and a +// constructor whose result is not assignable to T is rejected here, with the +// other configuration errors. A concrete constructor may therefore serve an +// interface key directly: s.Wire[Repository](NewPGRepo). The build calls ctor +// through reflect, which costs about 150ns and two allocations per build over +// a Provide closure; a warm Get is the same code for both. +func (s *Scope) Wire[T any](ctor any) Binding[T] { + want := reflect.TypeFor[T]() + fv, ft, fails := function("Wire["+typeName(want)+"]", "constructor", ctor, want) + wants := params(ft, 0) + b := s.register(key{t: want}, func(s *Scope) any { + args := make([]reflect.Value, len(wants)) + s.arguments(wants, args) + return call(fv, args, fails, want) + }) + b.wants = wants + return Binding[T]{s, b} +} + +// Wrap registers a wrapper over the registration that serves T when Wrap is +// called: the latest one in this scope, or the one an ancestor provides. fn +// takes the value being wrapped first and its other dependencies after it, +// read with reflection as Wire reads a constructor, and returns T, or T and +// an error: +// +// s.Wrap[Store](func(next Store, c *Cache) Store { return &caching{next, c} }) +// +// What is wrapped keeps its registration, hooks and lifetime: it is built +// first, as the wrapper's dependency, and so stopped after it. The wrapper +// serves T from this scope down. In a child scope it wraps the parent's value +// for that child and its descendants and leaves the parent and its other +// children as they were, which is what uber/fx calls Decorate. Wrappers +// chain in registration order, and a wrapper takes the lifetime of what it +// wraps; Scoped() on the wrapper makes it one per resolving scope over a +// shared inner value. An Override registered afterwards replaces the wrapper +// and everything it wrapped. Nothing to wrap is rejected here, and a group +// cannot be wrapped: its members are read with All. A key this scope has +// already resolved is rejected at the next resolution, as an Override is, +// since callers already hold the unwrapped value. +func (s *Scope) Wrap[T any](fn any) Binding[T] { + want := reflect.TypeFor[T]() + name := "Wrap[" + typeName(want) + "]" + fv, ft, fails := function(name, "wrapper", fn, want) + if ft.NumIn() == 0 || !want.AssignableTo(ft.In(0)) { + panic(fmt.Sprintf("di: %s: wrapper %s must take the %s it wraps as its first parameter", name, ft, typeName(want))) + } + k := key{t: want} + // This scope is read as it is, pending batch included, because committing + // the batch here would end it for every registration made so far. + // Ancestors are looked up as a resolution would look them up. + inner, at := s.current(k) + if inner == nil && s.parent != nil { + inner, at = (&Scope{state: s.parent}).lookup(k) + } + if inner == nil { + panic(fmt.Sprintf("di: %s: nothing provides %s in scope %s or above; a group is read with All and cannot be wrapped", name, k, s.name)) + } + wants := params(ft, 1) + b := s.register(k, func(s *Scope) any { + args := make([]reflect.Value, len(wants)+1) + // The wrapped value is resolved as a dependency, which records the + // edge, keeps build order and catches a wrapper that reaches back + // into itself; served is marked as get would mark it. + args[0] = argument(s.resolve(inner, at), ft.In(0)) + s.markServed(at, k) + s.arguments(wants, args[1:]) + return call(fv, args, fails, want) + }) + b.inner, b.innerAt, b.wants, b.scoped = inner, at, wants, inner.scoped + inner.wrappedBy.Store(b) + return Binding[T]{s, b} +} + +var errorType = reflect.TypeFor[error]() + +// function checks that fn is a non-variadic function returning want, or want +// and an error, and returns it with its type and whether it declares the +// error. name and role label the message: "di: Wire[*app.Server]: constructor +// must be a function". +func function(name, role string, fn any, want reflect.Type) (fv reflect.Value, ft reflect.Type, fails bool) { + fv = reflect.ValueOf(fn) + if !fv.IsValid() || fv.Kind() != reflect.Func { + panic(fmt.Sprintf("di: %s: %s must be a function, got %T", name, role, fn)) + } + ft = fv.Type() + switch { + case ft.IsVariadic(): + panic(fmt.Sprintf("di: %s: %s %s is variadic", name, role, ft)) + case ft.NumOut() == 0 || ft.NumOut() > 2: + panic(fmt.Sprintf("di: %s: %s %s must return T or (T, error)", name, role, ft)) + case !ft.Out(0).AssignableTo(want): + panic(fmt.Sprintf("di: %s: %s %s returns %s", name, role, ft, typeName(ft.Out(0)))) + case ft.NumOut() == 2 && ft.Out(1) != errorType: + panic(fmt.Sprintf("di: %s: %s %s must return T or (T, error)", name, role, ft)) + } + return fv, ft, ft.NumOut() == 2 +} + +// params lists the parameter types of ft from index from on, as keys. +func params(ft reflect.Type, from int) []key { + wants := make([]key, ft.NumIn()-from) + for i := range wants { + wants[i] = key{t: ft.In(i + from)} + } + return wants +} + +// arguments resolves each of wants from s into the corresponding slot of +// args. +func (s *Scope) arguments(wants []key, args []reflect.Value) { + for i, k := range wants { + args[i] = argument(s.get(k), k.t) + } +} + +// current is the registration serving k in this scope as of now, pending or +// committed, read without committing anything. +func (st *state) current(k key) (*binding, *state) { + st.mu.Lock() + defer st.mu.Unlock() + for _, b := range slices.Backward(st.pending) { + if b.key == k && !b.group { + return b, st + } + } + if b, ok := st.index[k]; ok { + return b, st + } + return nil, nil +} + +// argument makes a stored value into an argument of type t. A nil interface +// is a legitimate service, and reflect.ValueOf(nil) is not a value of any +// type; see as. +func argument(v any, t reflect.Type) reflect.Value { + if v == nil { + return reflect.Zero(t) + } + return reflect.ValueOf(v) +} + +// call runs a constructor through reflect and turns its error, if it +// declared one and returned it, into the abort that s.Must would raise. The +// value is stored as the registered type, not the constructor's result type: +// registration accepted any result assignable to the key, and a chan int +// stored for a <-chan int key would pass every check until Get asserted it. +// An interface key needs no conversion, since the assertion to an interface +// is what accepts the concrete value. +func call(fv reflect.Value, args []reflect.Value, fails bool, want reflect.Type) any { + out := fv.Call(args) + if fails && !out[1].IsNil() { + panic(abort{out[1].Interface().(error)}) + } + v := out[0] + if v.Type() != want && want.Kind() != reflect.Interface { + v = v.Convert(want) + } + return v.Interface() +} + +// edit applies a builder method to the binding, rejecting one made after the +// scope committed the registration. +func (b Binding[T]) edit(f func(*binding)) Binding[T] { + b.s.mu.Lock() + defer b.s.mu.Unlock() + if b.s.frozen && !slices.Contains(b.s.pending, b.b) { + panic(fmt.Sprintf("di: %s (provided at %s) modified after the scope was first resolved", b.b.key, b.b.where())) + } + f(b.b) + return b +} + +// Group makes the binding a member of the multi-binding group for T instead +// of the binding for T: it neither shadows nor is shadowed by another +// registration of T, and the members are read back together with s.All[T](). +// A member keeps its own lifetime and hooks. +func (b Binding[T]) Group() Binding[T] { + return b.edit(func(b *binding) { b.group = true }) +} + +// Override declares that this registration replaces an earlier one of the same +// key in the same scope. Without it a second registration of a key is rejected +// at the next resolution, naming both sites, because a duplicate that wins +// silently is how one module reroutes another module's wiring without anyone +// noticing. With it the later registration serves the key, and inherits its +// eagerness, which is the test seam: +// +// s := di.Test(t, app.Production) +// s.Value(&DB{DSN: "sqlite://memory"}).Override() +// +// There must be something to override in this scope, or that is rejected too: +// a fake for a service that has since been renamed would otherwise be a +// registration nobody resolves, and the test would pass against production +// wiring. A child scope shadows its parent without Override, since that is a +// different registry rather than a replacement. A key that has already served +// a value cannot be overridden at all. +func (b Binding[T]) Override() Binding[T] { + return b.edit(func(b *binding) { b.override = true }) +} + +// Scoped makes the binding one-per-scope: each scope that resolves it gets +// its own instance, built in that scope (so it can see that scope's +// values) and stopped with it. Declare request-scoped services once in the +// root and resolve them through the request scope. +func (b Binding[T]) Scoped() Binding[T] { + return b.edit(func(b *binding) { b.scoped = true }) +} + +// Eager builds the service during Start rather than on first use. +// +// Eagerness belongs to the key, not the registration: it means the service +// exists by the time Start returns. Overriding an eager binding therefore +// keeps the key eager and builds the replacement; a replacement with a +// per-scope lifetime, which cannot be built once at Start, is rejected. +func (b Binding[T]) Eager() Binding[T] { return b.edit(func(b *binding) { b.eager = true }) } + +// OnStart runs once the service is built, and only a hook that returns +// normally starts it: one that panics fails the start step, like a panicking +// constructor, and the service is never served. The hooks are typed: no +// interface sniffing, no reflection. +func (b Binding[T]) OnStart(f func(context.Context, T) error) Binding[T] { + return b.edit(func(b *binding) { b.onStart = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) +} + +// OnDrain runs before anything is stopped: Stop drains the whole tree, from +// the innermost scope outwards and in reverse build order, while every scope +// still resolves normally. It is where a service stops accepting new work and +// waits for the work it already has, such as an HTTP server that must finish +// in-flight requests whose handlers still need their request scope. Anything +// those handlers build, including a request scope of their own, is drained +// before the phase ends. Use OnStop for the release that follows. +func (b Binding[T]) OnDrain(f func(context.Context, T) error) Binding[T] { + return b.edit(func(b *binding) { b.onDrain = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) +} + +// OnStop releases the service, in reverse build order, once its drain step +// and its child scopes are done. It runs when OnStart succeeded, or when +// there is no OnStart to pair with, in which case it is a plain destructor; +// a service whose start step failed is not stopped. +func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T] { + return b.edit(func(b *binding) { b.onStop = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) +} + +// Worker registers a long-running function for T, such as a consumer loop. It is +// started in its own goroutine once the service starts and its context is +// cancelled when the service stops; Stop waits for it to return, bounded by +// its own context. A hook that outlasts that deadline is reported by Stop, +// and OnStop then waits for it rather than releasing the value underneath a +// worker still reading it. +// +// Returning a non-nil error calls Shutdown with it, stopping the application, +// even if the scope was already stopping: a worker may fail, flush while the +// scope winds down, and only then report. The exception is context.Canceled +// from a hook that was already cancelled, which is a worker reporting the +// cancellation and nothing else. A hook that wants to stay quiet during +// shutdown should return nil. +func (b Binding[T]) Worker(f func(context.Context, T) error) Binding[T] { + return b.edit(func(b *binding) { b.worker = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) +} diff --git a/di.go b/di.go index d649520..1b03e37 100644 --- a/di.go +++ b/di.go @@ -93,22 +93,13 @@ package di import ( "context" "errors" - "fmt" - "maps" - "os" - "os/signal" "reflect" "runtime" "slices" "strings" - "sync" - "sync/atomic" - "syscall" "time" ) -// ---- keys ------------------------------------------------------------------ - // key identifies a service: its Go type. Keys compare by reflect.Type // identity, so same-named types in different packages never collide (unlike // fmt.Sprintf("%T")-derived names). There is no name alongside the type: a @@ -140,16 +131,12 @@ func typeName(t reflect.Type) string { return t.String() } -// ---- errors ---------------------------------------------------------------- - var ( ErrNotProvided = errors.New("not provided") ErrCycle = errors.New("dependency cycle") ErrStopped = errors.New("scope stopped") ) -// ---- observability --------------------------------------------------------- - // EventKind classifies an Event. type EventKind string @@ -179,943 +166,6 @@ type Event struct { Err error } -type abort struct{ err error } - -// ---- state ----------------------------------------------------------------- - -type binding struct { - key key - site string - module string // the Module this was registered from, or "" - group bool - scoped bool - eager bool - override bool // declared to replace an earlier registration of the key - isValue bool // registered with Value: lifetimes do not apply - wants []key // the parameter types of a Wire constructor; nil for a Provide closure - build func(*Scope) any - - // inner is the registration a Wrap composes over, bound when Wrap is - // called, and innerAt the scope that registered it; both nil for any - // other binding. wrappedBy is set on a binding a Wrap has bound to: an - // Override that replaced it would leave the wrapper composing over a - // registration that no longer serves the key. - inner *binding - innerAt *state - wrappedBy atomic.Pointer[binding] - onStart func(context.Context, any) error - onDrain func(context.Context, any) error - onStop func(context.Context, any) error - worker func(context.Context, any) error - - // used is set once this binding has served a value. From then on the - // registration cannot be overridden, since that would leave two live - // instances of one service. A failed resolution built nothing and leaves - // the key re-registerable; that is how a key whose constructor failed is - // recovered. - used atomic.Bool - - // resolving counts the resolutions of this binding that have not served - // a value yet, which is the window used cannot cover. A constructor may - // register over its own key and resolve the replacement, and then the - // nested call is served the new value and the outer call the old one -- - // two live values for one key, from one goroutine, past a guard that only - // looked at used. It is kept only until the first value is served, since - // after that used says the same thing and costs nothing to read. - resolving atomic.Int32 - - single *instance // the singleton; scoped bindings keep one instance per state -} - -// where names the registration for a message: its site, and the module it was -// registered from when there is one. With modules in play, "provided at -// storage (wire.go:12) and again at caching (cache.go:8)" is the line that -// says what happened; two file positions alone do not. -func (b *binding) where() string { - if b.module == "" { - return b.site - } - return b.module + " (" + b.site + ")" -} - -// validate rejects lifetime and hook combinations that cannot be honoured. -// It runs at freeze, so the order the builder methods were called in does -// not matter. -func (b *binding) validate() { - bad := func(what, why string) { - panic(fmt.Sprintf("di: %s (provided at %s): %s %s", b.key, b.where(), what, why)) - } - switch { - case b.eager && b.scoped: - // Rejected even if a later registration overrides it. Whether an - // override inherits eagerness is decided in deriveEager. - bad("Eager", "does not apply to a Scoped binding: it is not built once") - case b.isValue && b.scoped: - bad("Scoped", "is meaningless for a Value binding: the instance already exists") - case b.group && b.override: - bad("Override", "does not apply to a group member: members accumulate rather than replace one another") - case b.inner != nil && b.group: - bad("Group", "does not apply to a wrapper: it serves the key it wraps") - case b.inner != nil && b.override: - bad("Override", "does not apply to a wrapper: it composes over the registration it wraps rather than replacing it") - } -} - -// phase is an instance's position in the build/start/stop sequence. It is -// read and written only under the owning state's mutex, so deciding who -// starts or stops an instance never spans two critical sections. -type phase int8 - -const ( - phaseNew phase = iota // no value yet - phaseBuilding // a resolution has claimed the build step - phaseBuilt // constructor ran; the start step has not - phaseStarting // a goroutine has claimed the start step - phaseStarted // the start step succeeded - phaseFailed // the build or the start step failed - phaseStopped // the stop step ran, or was skipped for good -) - -// drainPhase tracks OnDrain the way phase tracks the build and start steps. -// A drain in progress has to be waited for, so a waiter needs to tell it from -// one that has finished; a bare "decided" flag let a concurrent Stop skip the -// hook and run OnStop while the hook still held the value. -type drainPhase int8 - -const ( - drainNone drainPhase = iota // OnDrain has not been considered - draining // a Stop is running OnDrain now - drained // OnDrain ran, or was skipped for good -) - -// dep is one recorded dependency edge: an instance a constructor resolved, -// and the scope holding it, which is what names it in a rendering. The -// holder is carried rather than looked up because an instance does not know -// its own scope, and the scope it was resolved from may be gone by the time -// anything reads the edge. -type dep struct { - in *instance - holder *state -} - -// instance is one built value of a binding, owned by the state that stops it. -type instance struct { - b *binding - ph phase // guarded by the owning state's mutex - value any - err error // guarded by the owning state's mutex - // settled is set when the build step has finished and value and err are - // final. - settled bool // guarded by the owning state's mutex - dr drainPhase // guarded by the owning state's mutex - - // deps are the services this instance's constructor resolved, in the - // order it asked for them, each recorded once however many times it - // asked. Guarded by the owning state's mutex, because a constructor may - // resolve from several goroutines at once and they share the Scope it - // was handed. Only Explain and Graph read them; nothing in the - // build/start/stop machine does. - deps []dep - - // Each step another goroutine may have to wait for has a channel that is - // closed when the step is done, so a waiter blocks on that step alone. - // The first goroutine that has to wait makes the channel (waitOn); the - // owner of the step closes it if it exists (wake). Both happen under the - // owning state's mutex, in the same critical section as the phase change, - // so either order is safe: a waiter that arrives first is released by the - // close, and an owner that finishes first leaves nil behind, in which case - // the phase already says the step is done and the waiter never blocks. - // - // Nil is the normal state. An uncontended build, an unraced start step - // and an undisputed drain allocate nothing. Never block on one of these - // fields directly, since a receive from a nil channel blocks for ever; - // go through waitOn. - settledCh chan struct{} // closed by settle: value and err are final - startingCh chan struct{} // closed when the start step is no longer in flight - drainedCh chan struct{} // closed when OnDrain has finished - - // builder is the resolution running the build step, guarded by the - // container graph's mutex. It is the edge that makes a cycle between - // concurrent builds visible. - builder *resolver - - // Worker hook bookkeeping, guarded by the phase machine rather than a mutex. - // cancel and runDone are written by start, on the goroutine that owns the - // start step, and read by stop, which stopIfNeeded reaches only after the - // start step has left phaseStarting -- in startClaimed's deferred block, - // under the owning state's mutex, after start returned. That lock handoff - // is the happens-before. runErr is written by the worker goroutine before it - // closes runDone and read only after a receive from runDone. - cancel context.CancelFunc - runDone chan struct{} - runErr error -} - -// hookKey marks a context as belonging to a lifecycle hook. -type hookKey struct{} - -// inHook tags the context a hook is called with, so a Stop made with that -// context can name the misuse instead of waiting for a step the caller is -// itself responsible for finishing. A hook that passes some other context -// keeps the plain bounded wait; this catches the case worth catching, which -// is the hook passing on the context it was handed. -func inHook(ctx context.Context, st *state) context.Context { - return context.WithValue(ctx, hookKey{}, st) -} - -// hookOwner returns the scope whose hook ctx belongs to, or nil. -func hookOwner(ctx context.Context) *state { - st, _ := ctx.Value(hookKey{}).(*state) - return st -} - -// descendsFrom reports whether st is anc or a scope under it. -func (st *state) descendsFrom(anc *state) bool { - for ; st != nil; st = st.parent { - if st == anc { - return true - } - } - return false -} - -// wake releases whoever is waiting for a step. A nil channel means nobody had -// to wait; see the channel fields on instance. -func wake(ch chan struct{}) { - if ch != nil { - close(ch) - } -} - -// waitOn returns the channel to block on for an outstanding step, making it -// on first use. Must be called with the owning state's mutex held, in the same -// critical section that read the phase, so the owner cannot close the channel -// in between. -func waitOn(ch *chan struct{}) chan struct{} { - if *ch == nil { - *ch = make(chan struct{}) - } - return *ch -} - -// graph is one container's wait-for graph, with two kinds of edge: an -// instance points at the resolution building it (instance.builder), and a -// blocked resolution points at the instance it waits for (blockedFor). Its -// mutex is the innermost lock: a state's mutex may be held while taking it, -// never the reverse, so the graph can be read across scopes without ordering -// state mutexes against each other. -// -// New makes one graph per container and every scope under that root shares -// it. That is as far as a cycle can reach: a resolution follows the parent -// chain, so a wait can cross scopes, but nothing joins two containers. -type graph struct { - mu sync.Mutex - blockedFor map[*resolver]*instance -} - -// descends reports whether n is anc or was created below it. A branch blocks -// at a leaf of its path, several nodes below the one that claimed the build it -// is holding up, so both directions of the graph are matched against whole -// paths rather than single nodes. -// -// The walk stops at a node whose resolution has returned, for the same reason -// onPath does: nothing above a finished node is waiting for what is opened -// below it later. -func descends(n, anc *resolver) bool { - for ; n != nil; n = n.parent { - if n == anc { - return true - } - if n.done.Load() { - return false - } - } - return false -} - -// wait records that r is about to wait for in, unless that would close a -// wait-for cycle by reaching, through builds that are themselves blocked, a -// build this branch is responsible for finishing. Called with the holder's -// mutex held; the check and the new edge are one critical section, so two -// branches closing a cycle at once cannot both decide to wait. -func (r *resolver) wait(g *graph, in *instance) bool { - g.mu.Lock() - defer g.mu.Unlock() - - seen := map[*instance]bool{in: true} - for stack := []*instance{in}; len(stack) > 0; { - cur := stack[len(stack)-1] - stack = stack[:len(stack)-1] - builder := cur.builder - if builder == nil { - continue // nobody is building it: whoever holds it will settle it - } - if descends(r, builder) { - return false // waiting on our own branch's work - } - for n, j := range g.blockedFor { - if descends(n, builder) && !seen[j] { - seen[j] = true - stack = append(stack, j) - } - } - } - g.blockedFor[r] = in - return true -} - -func (r *resolver) unwait(g *graph) { - g.mu.Lock() - delete(g.blockedFor, r) - g.mu.Unlock() -} - -// claimBuild takes the build step for this resolution. Called with the -// holder's mutex held. -func (in *instance) claimBuild(holder *state, r *resolver) { - in.ph = phaseBuilding - g := holder.graph - g.mu.Lock() - in.builder = r - g.mu.Unlock() -} - -// settle publishes the outcome of the build step and wakes every resolution -// waiting for this instance. -func (in *instance) settle(holder *state) { - holder.mu.Lock() - in.settled = true - g := holder.graph - g.mu.Lock() - in.builder = nil - g.mu.Unlock() - wake(in.settledCh) - holder.mu.Unlock() -} - -// fail records a build failure, which is terminal for the instance. -func (in *instance) fail(holder *state, err error) { - holder.mu.Lock() - in.ph, in.err = phaseFailed, err - holder.mu.Unlock() -} - -// start runs OnStart and launches the Worker hook. The worker's context is detached -// from ctx so the worker is cancelled by Stop, in dependency order, rather -// than the moment the application context is cancelled. -func (in *instance) start(ctx context.Context, owner *state) error { - b := in.b - if b.onStart != nil { - t0 := time.Now() - err := b.onStart(inHook(ctx, owner), in.value) - owner.emit(Event{Kind: EventStart, Service: b.key.String(), Package: b.key.pkgPath(), Scope: owner.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: err}) - if err != nil { - return err - } - } - if b.worker != nil { - rctx, cancel := context.WithCancel(context.WithoutCancel(ctx)) - in.cancel, in.runDone = cancel, make(chan struct{}) - hctx := inHook(rctx, owner) - go func() { - defer close(in.runDone) - err := b.worker(hctx, in.value) - if err == nil { - return - } - if rctx.Err() != nil && onlyCancellation(err) { - return // we cancelled it and it reported just that - } - // Any other error is the worker's own failure and goes to - // Shutdown, whether or not the scope had begun stopping. Only - // the returned error says so: rctx.Err() says whether we - // cancelled, not why the worker failed, and a worker that fails, - // flushes until told to stop and then reports returns after the - // cancellation without owing anything to it. - // - // Wrap once and keep that value. Stop reports it and Run - // receives it, so the two recognise one failure instead of - // listing it twice. Stop alone is not enough: the owning scope - // may be a child that detaches before an ancestor's Stop reaches - // it, and whoever called that Stop may discard its result. - in.runErr = fmt.Errorf("di: %s: %w", b.key, err) - (&Scope{state: owner}).Shutdown(in.runErr) - }() - } - return nil -} - -// claim takes the start step for this goroutine, returning false if another -// one already has it or the instance is past starting. -func (in *instance) claim(owner *state) bool { - owner.mu.Lock() - defer owner.mu.Unlock() - if in.ph != phaseBuilt { - return false - } - in.ph = phaseStarting - return true -} - -// callHook runs a lifecycle hook and reports what it did as an error, a panic -// included. A hook that panics -- or resolves something whose registration is -// rejected, which reaches it as a panic -- must not take the teardown down -// with it: the drain sweep would stop halfway, stopOnce would be claimed and -// never settled, every later Stop would wait for it until its context ran -// out, and every instance behind it would never be released. The start step -// has always been recovered this way; the drain and stop steps were not, and a -// config rejection raised inside a drain hook was how that showed. -func callHook(hook func(context.Context, any) error, ctx context.Context, v any) (err error) { - defer func() { - if rec := recover(); rec != nil { - if a, ok := rec.(abort); ok { - err = a.err // a nested resolution failed; report that cause - } else { - err = fmt.Errorf("panic: %v", rec) - } - } - }() - return hook(ctx, v) -} - -// startClaimed runs the start step of an instance already in phaseStarting. -// The phase is settled even if the hook panics, which is what releases a Stop -// or a resolution waiting for the step. The failure is recorded on the -// instance as well as returned, so a resolution that waited reports the same -// error. -// -// A panicking hook is a failed start, converted to an error like a panicking -// constructor: only a hook that returned has started its service. Otherwise a -// caller that recovered the panic would be served a half-initialised service, -// and Stop would pair an OnStop with an OnStart that never finished. -func (in *instance) startClaimed(ctx context.Context, owner *state) (err error) { - defer func() { - if rec := recover(); rec != nil { - if a, ok := rec.(abort); ok { - err = a.err // a nested resolution failed; report that cause - } else { - err = fmt.Errorf("panic: %v", rec) - } - } - owner.mu.Lock() - if err == nil { - in.ph = phaseStarted - } else { - in.ph = phaseFailed - if in.err == nil { - in.err = fmt.Errorf("di: starting %s (provided at %s): %w", in.b.key, in.b.where(), err) - } - } - wake(in.startingCh) // the start step is no longer in flight - owner.mu.Unlock() - }() - return in.start(ctx, owner) -} - -// everStarted reports whether Start was called on this scope or an ancestor. -// It is runContext's walk with the answer discarded; the two questions are -// the same search. -func (st *state) everStarted() bool { - ctx, _ := st.runContext() - return ctx != nil -} - -// stopContext returns the context Stop was called with, or a background one -// if the scope was stopped without recording it. -func (st *state) stopContext() context.Context { - for ; st != nil; st = st.parent { - st.mu.Lock() - ctx := st.stopCtx - st.mu.Unlock() - if ctx != nil { - return ctx - } - } - return context.Background() -} - -// stopIfNeeded runs the stop step once, when it is owed: when the start step -// succeeded, or when the instance was built and its OnStop has no OnStart to -// pair with, either because the binding declares none or because the scope -// was never started, so OnStop is a plain destructor. An instance whose -// OnStart failed or was skipped by a rollback is not stopped. -// -// It first waits out whichever step another goroutine is still running for -// this instance, so the release never runs against a value one of them holds: -// a start step in flight, and then a drain hook, since a drain waits for the -// start step too and the two cannot overlap. Waiting for the start step is -// what makes Stop synchronous. It is safe because a hook may not call Stop on -// its own scope or an ancestor -- that wait would be on itself -- and Stop -// rejects the case it can see rather than hanging. -// -// If ctx expires first the release is still owed, and this is the only caller -// that can make it happen: Stop took the instance off its scope's list before -// the walk began. So the deadline ends the caller's wait, not the teardown, -// which finishes on a goroutine of its own once the step returns. -// WithoutCancel keeps the context's values and drops the spent deadline, so -// that goroutine waits properly and then releases. -func (in *instance) stopIfNeeded(ctx context.Context, owner *state) error { - // everStarted walks the parent chain, so it is answered before the - // owner's mutex is taken rather than under it. - paired := in.b.onStart != nil && owner.everStarted() - for { - owner.mu.Lock() - step, what := in.outstanding() - if step == nil { - owed := in.ph == phaseStarted || (in.ph == phaseBuilt && !paired) - in.ph = phaseStopped - owner.mu.Unlock() - if !owed { - return nil - } - return in.stop(ctx, owner) - } - owner.mu.Unlock() - select { - case <-step: - // One step done; the next may be outstanding now. - case <-ctx.Done(): - go func() { _ = in.stopIfNeeded(context.WithoutCancel(ctx), owner) }() - return fmt.Errorf("di: stopping %s: %s did not return: %w", in.b.key, what, ctx.Err()) - } - } -} - -// outstanding names the step another goroutine is running for this instance, -// with the channel that goroutine will close, or nil if the instance is -// nobody else's business. Called with the owning state's mutex held, so the -// phase and the channel are read in one critical section. -func (in *instance) outstanding() (chan struct{}, string) { - switch { - case in.ph == phaseStarting: - return waitOn(&in.startingCh), "OnStart" - case in.dr == draining: - return waitOn(&in.drainedCh), "OnDrain" - } - return nil, "" -} - -// drainIfNeeded runs OnDrain once, under the same predicate as stopIfNeeded: -// a service that will not be stopped has nothing to wind down. It reports -// whether this call ran or waited for the hook, so a drain pass can tell -// that it did work. -// -// A drain another Stop has begun is waited for, not skipped, or this Stop -// would go on to run OnStop while that hook still holds the value. A start -// step in flight is waited for as well, for the same reason stopIfNeeded -// waits for it. -func (in *instance) drainIfNeeded(ctx context.Context, owner *state) (bool, error) { - b := in.b - if b.onDrain == nil { - return false, nil - } - paired := b.onStart != nil && owner.everStarted() - for { - owner.mu.Lock() - if in.dr == drained { - owner.mu.Unlock() - return false, nil - } - if in.dr == draining { - done := waitOn(&in.drainedCh) - owner.mu.Unlock() - select { - case <-done: - return true, nil - case <-ctx.Done(): - return true, fmt.Errorf("di: draining %s: another Stop did not finish OnDrain: %w", b.key, ctx.Err()) - } - } - if in.ph == phaseStarting { - // The start step is in flight on another goroutine. Wait for it: - // a service that is starting owes a drain as soon as it has - // started, and leaving it undecided for a later pass meant a - // step that outlasted the phase was never drained at all. - starting := waitOn(&in.startingCh) - owner.mu.Unlock() - select { - case <-starting: - continue - case <-ctx.Done(): - return false, fmt.Errorf("di: draining %s: OnStart did not return: %w", b.key, ctx.Err()) - } - } - if owner.isStopped() { - // The scope's own Stop has moved past draining. A sweep still - // running in an ancestor can reach an instance built into such a - // scope; draining it would run the hook against a scope that no - // longer resolves, for work it can no longer take on. - in.dr = drained - owner.mu.Unlock() - return false, nil - } - if owed := in.ph == phaseStarted || (in.ph == phaseBuilt && !paired); !owed { - // Straight to drained, never draining, so no waiter can arrive - // and no channel is needed. - in.dr = drained - owner.mu.Unlock() - return false, nil - } - in.dr = draining - owner.mu.Unlock() - break - } - - t0 := time.Now() - err := callHook(b.onDrain, inHook(ctx, owner), in.value) - owner.emit(Event{Kind: EventDrain, Service: b.key.String(), Package: b.key.pkgPath(), Scope: owner.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: err}) - - owner.mu.Lock() - in.dr = drained - wake(in.drainedCh) // release a concurrent Stop waiting for this hook - owner.mu.Unlock() - - if err != nil { - return true, fmt.Errorf("di: draining %s: %w", b.key, err) - } - return true, nil -} - -// stop cancels the Worker hook, waits for it within ctx, then runs OnStop. -// -// A Worker hook that outlasts ctx still holds the value, so OnStop cannot run yet -// without racing the worker. The missed deadline is reported to the caller and -// the release is finished when the worker returns, as Stop does for a start -// step in flight. -func (in *instance) stop(ctx context.Context, owner *state) error { - b := in.b - if in.cancel == nil && b.onStop == nil { - return nil - } - t0 := time.Now() - var errs []error - if in.cancel != nil { - in.cancel() - select { - case <-in.runDone: - if in.runErr != nil { - errs = append(errs, in.runErr) - } - case <-ctx.Done(): - err := fmt.Errorf("di: stopping %s: Worker hook did not return: %w", b.key, ctx.Err()) - if b.onStop == nil { - owner.emit(Event{Kind: EventStop, Service: b.key.String(), Package: b.key.pkgPath(), Scope: owner.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: err}) - return err - } - // WithoutCancel keeps the values and drops the spent deadline; the - // caller has stopped waiting, so what is left is best-effort. - go in.releaseAfterWorker(context.WithoutCancel(ctx), owner, err) - return err - } - } - if b.onStop != nil { - if err := callHook(b.onStop, inHook(ctx, owner), in.value); err != nil { - errs = append(errs, fmt.Errorf("di: stopping %s: %w", b.key, err)) - } - } - err := errors.Join(errs...) - owner.emit(Event{Kind: EventStop, Service: b.key.String(), Package: b.key.pkgPath(), Scope: owner.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: err}) - return err -} - -// releaseAfterWorker finishes a stop step whose Worker hook outlasted Stop's -// context, once the hook returns. missed is what Stop returned to its caller; -// the instance's single EventStop is emitted here and carries it along with -// the release's own result, so no observer sees a service stopped twice. -func (in *instance) releaseAfterWorker(ctx context.Context, owner *state, missed error) { - <-in.runDone - b := in.b - t0 := time.Now() - errs := []error{missed} - if in.runErr != nil { - errs = append(errs, in.runErr) - } - if err := callHook(b.onStop, inHook(ctx, owner), in.value); err != nil { - errs = append(errs, fmt.Errorf("di: stopping %s: %w", b.key, err)) - } - owner.emit(Event{Kind: EventStop, Service: b.key.String(), Package: b.key.pkgPath(), Scope: owner.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: errors.Join(errs...)}) -} - -// once is a teardown phase that runs at most once per scope: the first caller -// runs it, and every later or concurrent caller waits for that run, bounded by -// its own context. Stop and the scope-wide drain are both this shape. -// -// Its fields are guarded by the state's mutex, so claiming the phase and -// recording what the claim decided are one critical section, and no third -// lock joins the ordering rules. -type once struct { - done chan struct{} // made by the claimer, closed once its run has finished - err error // that run's result -} - -// claim reports whether this caller owns the run. The owner must call settle -// exactly once; everyone else calls wait. claimed, if non-nil, runs under the -// mutex in the same critical section that picks the winner, for state a waiter -// must see as soon as it sees the phase claimed. -func (o *once) claim(st *state, claimed func()) bool { - st.mu.Lock() - defer st.mu.Unlock() - if o.done != nil { - return false - } - o.done = make(chan struct{}) - if claimed != nil { - claimed() - } - return true -} - -// settle publishes the run's result and releases the waiters. -func (o *once) settle(st *state, err error) { - st.mu.Lock() - o.err = err - st.mu.Unlock() - close(o.done) -} - -// wait blocks until the owning run has finished and reports its error, or -// reports false if the caller's context expires first. Only a caller whose -// claim returned false may wait: an unclaimed phase has no channel and would -// block until ctx expires. -func (o *once) wait(st *state, ctx context.Context) (error, bool) { - st.mu.Lock() - done := o.done - st.mu.Unlock() - select { - case <-done: - case <-ctx.Done(): - return nil, false - } - st.mu.Lock() - defer st.mu.Unlock() - return o.err, true -} - -type state struct { - name string - parent *state - graph *graph // the container's wait-for graph, shared with every other scope under the root - - mu sync.Mutex - pending []*binding // registrations not yet indexed - index map[key]*binding - groups map[key][]*binding - frozen bool - all []*binding // every binding, in registration order - eager []*binding // derived by deriveEager: what Start builds - started []*instance // build order; stopped in reverse - scoped map[*binding]*instance // per-scope instances of Scoped bindings - served map[key]bool // keys this scope resolved from an outer scope; lazily made - children []*state - observers []func(Event) - - stopped atomic.Bool // set by Stop or a failed Start; resolution then fails with ErrStopped - stopCtx context.Context // the context Stop was called with - stopOnce once // this scope's teardown; later Stop calls wait for it - startCtx context.Context // set by Start; read by Context() - running bool // set once Start reaches the hook phase; enables late OnStart - - // drainOnce is the scope-wide drain phase, once-with-wait like stopOnce: - // a second Stop reaching this scope waits for the first drain instead of - // running its own or skipping past it. - drainOnce once - - shutdownOnce sync.Once - shutdownCh chan struct{} - shutdownErr error -} - -// freeze commits the pending registrations. The batch is validated against a -// copy of the registry and committed only if it passes, so a rejected -// registration leaves the scope as it was and is rejected identically on -// every later attempt. -func (st *state) freeze() { - st.mu.Lock() - defer st.mu.Unlock() - if len(st.pending) == 0 { - return - } - - index := maps.Clone(st.index) - groups := maps.Clone(st.groups) - all := slices.Clone(st.all) - for _, b := range st.pending { - // A wrapper is built where what it wraps is built, so it takes that - // lifetime, read here rather than at registration because the - // wrapped binding's own Scoped() may come later in the batch. - if b.inner != nil && b.inner.scoped { - b.scoped = true - } - b.validate() - if b.group { - groups[b.key] = append(slices.Clone(groups[b.key]), b) - } else { - prev, ok := index[b.key] - act := "overridden" - if b.inner != nil { - act = "wrapped" - } - switch { - case ok && !b.override && b.inner == nil: - // Two registrations of one key in one scope, and the second - // did not say it meant to replace the first. Silently - // letting the later one win was how an unrelated module - // could reroute another module's wiring without a word said; - // a replacement is a thing a caller declares. - panic(fmt.Sprintf("di: %s is provided at %s and again at %s: a second registration of a key must be marked Override() to replace the first", - b.key, prev.where(), b.where())) - case !ok && b.override: - // An Override with nothing to override is nearly always a - // fake for a service that was renamed or removed, and the - // test it lives in would otherwise pass against production - // wiring. A child shadows its parent without Override: that - // is a different registry, not a replacement. - panic(fmt.Sprintf("di: %s (provided at %s) is marked Override() but nothing in scope %s provides it; a child scope shadows its parent without Override", - b.key, b.where(), st.name)) - case ok && prev.used.Load(): - // Replacing or wrapping a key that has served a value would - // leave two live instances of one service. - panic(fmt.Sprintf("di: %s (provided at %s) cannot be %s at %s: it has already been resolved", - b.key, prev.where(), act, b.where())) - case ok && prev.resolving.Load() > 0: - // A resolution of this key is in flight. Serving the - // replacement to anything it goes on to build, while the - // resolution that is running returns the old value, is the - // same two-live-values defect the check above prevents. - panic(fmt.Sprintf("di: %s (provided at %s) cannot be %s at %s: it is being resolved", - b.key, prev.where(), act, b.where())) - case ok && b.inner == nil && prev.wrappedBy.Load() != nil: - // A wrapper, in this scope or a descendant, composes over - // prev. Replacing prev would leave that wrapper serving a - // value built from a registration nothing else can reach. - panic(fmt.Sprintf("di: %s (provided at %s) cannot be overridden at %s: it is wrapped at %s", - b.key, prev.where(), b.where(), prev.wrappedBy.Load().where())) - } - if st.served[b.key] { - // This scope already served the key from an outer scope; - // shadowing it now would give one key two live values here. - panic(fmt.Sprintf("di: %s cannot be registered at %s: this scope has already resolved it from an outer scope", - b.key, b.where())) - } - index[b.key] = b - } - all = append(all, b) - } - eager := deriveEager(all, index) - - st.index, st.groups, st.all, st.eager = index, groups, all, eager - st.pending, st.frozen = nil, true -} - -// deriveEager returns the ordered set of bindings Start builds, and is the -// one place that decides what Eager means. For every key with an Eager -// registration, the set holds the binding that serves that key, once, at the -// position of the first such registration. A group member is its own entry. A -// binding with a per-scope lifetime cannot honour eagerness and is rejected -// here, whether declared so directly or arriving through an override. -func deriveEager(all []*binding, index map[key]*binding) []*binding { - var eager []*binding - seen := make(map[*binding]bool, len(all)) - for _, b := range all { - if !b.eager { - continue - } - w := b - if !b.group { - if w = index[b.key]; w == nil { - continue - } - } - if seen[w] { - continue - } - if w.scoped { - // b itself is caught by validate, so w is an override here. - panic(fmt.Sprintf("di: %s is Eager (provided at %s), but the Scoped registration at %s owns the key: eagerness cannot transfer to a per-scope lifetime", - b.key, b.where(), w.where())) - } - seen[w] = true - eager = append(eager, w) - } - return eager -} - -// resolver is one node of a resolution path: what is being resolved and the -// node that needed it. The path is a linked list, not a slice, because a -// constructor may resolve from several goroutines at once; a node is never -// mutated after it is made (except done), so branches share nothing and each -// carries the whole path for cycle detection and error messages. -// -// A node is identified by binding and holder, not by key: a group member and -// a plain registration of the same type are different bindings, and one -// Scoped binding is a different node in each scope that holds an instance of -// it. The key is read back from the binding, which is where it lives; the -// node carried its own copy while a Bind alias could put a hop on the path -// whose key was not its binding's. -type resolver struct { - parent *resolver - b *binding // nil on the root node, which resolves nothing itself - holder *state - - // done marks a node whose resolution has returned. The path stays whole - // for error messages, but a finished node is no longer a dependency: a - // constructor may keep the Scope it was handed and resolve through it - // later, and that resolution must not meet its own finished frame and be - // called a cycle. Written once by the resolution that owns the node, read - // from any branch. - done atomic.Bool -} - -func (r *resolver) child(b *binding, holder *state) *resolver { - return &resolver{parent: r, b: b, holder: holder} -} - -// onPath reports whether this exact binding is still being resolved further -// up the path, which is a dependency cycle within one branch. -// -// The walk stops at the first finished node rather than skipping it. Only a -// constructor that kept its Scope can put a finished node on a live path, and -// a resolution made through that Scope afterwards is a new branch: the -// ancestors above the finished node may still be building, but not for it, -// so it has only to wait for them. Counting them reported a false cycle and -// cached it on whatever was being built. -// -// The case this gives up cannot be told apart without goroutine-local state, -// as with Stop's mid-start handoff: a constructor that blocks, itself or -// through a goroutine it waits for, on a resolution made through a finished -// descendant's Scope that leads back to it. That used to be reported as a -// cycle and now deadlocks. It requires a service to reach back into its own -// unfinished construction through a Scope that escaped a nested constructor. -func (r *resolver) onPath(b *binding, holder *state) bool { - for n := r; n != nil; n = n.parent { - if n.done.Load() { - return false - } - if n.b == b && n.holder == holder { - return true - } - } - return false -} - -func (r *resolver) path() string { - if r == nil || r.b == nil { - return "" - } - return " (needed by " + r.pathStr() + ")" -} - -func (r *resolver) pathStr() string { - var parts []string - for n := r; n != nil; n = n.parent { - if n.b != nil { - parts = append(parts, n.b.key.String()) - } - } - slices.Reverse(parts) - return fmt.Sprint(parts) -} - // Scope is a container. A Scope value handed to a constructor is a view over // the same state that carries the current resolution path. type Scope struct { @@ -1124,6 +174,7 @@ type Scope struct { module string // the Module registering through this handle, or "" } +// New creates a root scope: a container with no parent. func New() *Scope { return &Scope{state: newState("root", nil)} } func newState(name string, parent *state) *state { @@ -1215,1080 +266,16 @@ func (st *state) emit(ev Event) { } } -func callsite() string { - _, file, line, _ := runtime.Caller(3) - return fmt.Sprintf("%s:%d", file, line) -} - -// ---- registration (generic methods) --------------------------------------- - -// Binding is the typed handle returned by Provide/Value. Its -// methods refine the registration; they must be called before the first -// resolution from this scope. -type Binding[T any] struct { - s *Scope - b *binding -} - -func (s *Scope) register(k key, build func(*Scope) any) *binding { - b := &binding{key: k, site: callsite(), module: s.module, build: build} - b.single = &instance{b: b} - s.mu.Lock() - s.pending = append(s.pending, b) - s.mu.Unlock() - return b -} - -// Provide registers a lazily built singleton. T is inferred from the -// constructor's return type; dependencies are pulled with s.Get[...](). -func (s *Scope) Provide[T any](ctor func(*Scope) T) Binding[T] { - return Binding[T]{s, s.register(key{t: reflect.TypeFor[T]()}, func(s *Scope) any { return ctor(s) })} -} - -// Value registers an already-built instance. -func (s *Scope) Value[T any](v T) Binding[T] { - b := s.register(key{t: reflect.TypeFor[T]()}, func(*Scope) any { return v }) - b.isValue = true - return Binding[T]{s, b} -} - -// Wire registers a lazily built singleton from a constructor of any arity, -// whose parameters are its dependencies: -// -// s.Wire[*Server](NewServer) // func NewServer(cfg Config, repo *Repo) *Server -// -// ctor must be a non-variadic function returning T, or T and an error, and is -// read with reflection once, here. Each parameter type is resolved from the -// same scope view a Provide closure would see, so lifetimes, cycles, hooks and -// error paths are unchanged; what Wire adds is that the dependencies are known -// at registration, before anything is built. A non-nil error from ctor aborts -// the build exactly as s.Must does. -// -// T cannot be inferred from an untyped argument, so it is spelled out, and a -// constructor whose result is not assignable to T is rejected here, with the -// other configuration errors. A concrete constructor may therefore serve an -// interface key directly: s.Wire[Repository](NewPGRepo). The build calls ctor -// through reflect, which costs about 150ns and two allocations per build over -// a Provide closure; a warm Get is the same code for both. -func (s *Scope) Wire[T any](ctor any) Binding[T] { - fv := reflect.ValueOf(ctor) - if !fv.IsValid() || fv.Kind() != reflect.Func { - panic(fmt.Sprintf("di: Wire[%s]: constructor must be a function, got %T", typeName(reflect.TypeFor[T]()), ctor)) - } - ft := fv.Type() - want := reflect.TypeFor[T]() - switch { - case ft.IsVariadic(): - panic(fmt.Sprintf("di: Wire[%s]: constructor %s is variadic", typeName(want), ft)) - case ft.NumOut() == 0 || ft.NumOut() > 2: - panic(fmt.Sprintf("di: Wire[%s]: constructor %s must return T or (T, error)", typeName(want), ft)) - case !ft.Out(0).AssignableTo(want): - panic(fmt.Sprintf("di: Wire[%s]: constructor %s returns %s", typeName(want), ft, typeName(ft.Out(0)))) - case ft.NumOut() == 2 && ft.Out(1) != errorType: - panic(fmt.Sprintf("di: Wire[%s]: constructor %s must return T or (T, error)", typeName(want), ft)) - } - wants := make([]key, ft.NumIn()) - for i := range wants { - wants[i] = key{t: ft.In(i)} - } - fails := ft.NumOut() == 2 - b := s.register(key{t: want}, func(s *Scope) any { - args := make([]reflect.Value, len(wants)) - for i, k := range wants { - args[i] = argument(s.get(k), k.t) - } - return call(fv, args, fails, want) - }) - b.wants = wants - return Binding[T]{s, b} -} - -// Wrap registers a wrapper over the registration that serves T when Wrap is -// called: the latest one in this scope, or the one an ancestor provides. fn -// takes the value being wrapped first and its other dependencies after it, -// read with reflection as Wire reads a constructor, and returns T, or T and -// an error: -// -// s.Wrap[Store](func(next Store, c *Cache) Store { return &caching{next, c} }) -// -// What is wrapped keeps its registration, hooks and lifetime: it is built -// first, as the wrapper's dependency, and so stopped after it. The wrapper -// serves T from this scope down. In a child scope it wraps the parent's value -// for that child and its descendants and leaves the parent and its other -// children as they were, which is what uber/fx calls Decorate. Wrappers -// chain in registration order, and a wrapper takes the lifetime of what it -// wraps; Scoped() on the wrapper makes it one per resolving scope over a -// shared inner value. An Override registered afterwards replaces the wrapper -// and everything it wrapped. Nothing to wrap is rejected here, and a group -// cannot be wrapped: its members are read with All. A key this scope has -// already resolved is rejected at the next resolution, as an Override is, -// since callers already hold the unwrapped value. -func (s *Scope) Wrap[T any](fn any) Binding[T] { - want := reflect.TypeFor[T]() - name := "Wrap[" + typeName(want) + "]" - fv := reflect.ValueOf(fn) - if !fv.IsValid() || fv.Kind() != reflect.Func { - panic(fmt.Sprintf("di: %s: wrapper must be a function, got %T", name, fn)) - } - ft := fv.Type() - switch { - case ft.IsVariadic(): - panic(fmt.Sprintf("di: %s: wrapper %s is variadic", name, ft)) - case ft.NumIn() == 0 || !want.AssignableTo(ft.In(0)): - panic(fmt.Sprintf("di: %s: wrapper %s must take the %s it wraps as its first parameter", name, ft, typeName(want))) - case ft.NumOut() == 0 || ft.NumOut() > 2: - panic(fmt.Sprintf("di: %s: wrapper %s must return T or (T, error)", name, ft)) - case !ft.Out(0).AssignableTo(want): - panic(fmt.Sprintf("di: %s: wrapper %s returns %s", name, ft, typeName(ft.Out(0)))) - case ft.NumOut() == 2 && ft.Out(1) != errorType: - panic(fmt.Sprintf("di: %s: wrapper %s must return T or (T, error)", name, ft)) - } - k := key{t: want} - // This scope is read as it is, pending batch included and committed - // nothing, because committing it here would end the batch for every - // registration made so far. Ancestors are looked up as a resolution - // would look them up. - inner, at := s.current(k) - if inner == nil && s.parent != nil { - inner, at = (&Scope{state: s.parent}).lookup(k) - } - if inner == nil { - panic(fmt.Sprintf("di: %s: nothing provides %s in scope %s or above; a group is read with All and cannot be wrapped", name, k, s.name)) - } - wants := make([]key, ft.NumIn()-1) - for i := range wants { - wants[i] = key{t: ft.In(i + 1)} - } - fails := ft.NumOut() == 2 - b := s.register(k, func(s *Scope) any { - args := make([]reflect.Value, len(wants)+1) - // The wrapped value is resolved as a dependency, which records the - // edge, keeps build order and catches a wrapper that reaches back - // into itself; served is marked as get would mark it. - args[0] = argument(s.resolve(inner, at), ft.In(0)) - s.markServed(at, k) - for i, k := range wants { - args[i+1] = argument(s.get(k), k.t) - } - return call(fv, args, fails, want) - }) - b.inner, b.innerAt, b.wants, b.scoped = inner, at, wants, inner.scoped - inner.wrappedBy.Store(b) - return Binding[T]{s, b} -} - -// current is the registration serving k in this scope as of now, pending or -// committed, read without committing anything. -func (st *state) current(k key) (*binding, *state) { - st.mu.Lock() - defer st.mu.Unlock() - for _, b := range slices.Backward(st.pending) { - if b.key == k && !b.group { - return b, st - } - } - if b, ok := st.index[k]; ok { - return b, st - } - return nil, nil -} - -// argument makes a stored value into an argument of type t. A nil interface -// is a legitimate service, and reflect.ValueOf(nil) is not a value of any -// type; see as. -func argument(v any, t reflect.Type) reflect.Value { - if v == nil { - return reflect.Zero(t) - } - return reflect.ValueOf(v) -} - -// call runs a constructor through reflect and turns its error, if it -// declared one and returned it, into the abort that s.Must would raise. The -// value is stored as the registered type, not the constructor's result type: -// registration accepted any result assignable to the key, and a chan int -// stored for a <-chan int key would pass every check until Get asserted it. -// An interface key needs no conversion, since the assertion to an interface -// is what accepts the concrete value. -func call(fv reflect.Value, args []reflect.Value, fails bool, want reflect.Type) any { - out := fv.Call(args) - if fails && !out[1].IsNil() { - panic(abort{out[1].Interface().(error)}) - } - v := out[0] - if v.Type() != want && want.Kind() != reflect.Interface { - v = v.Convert(want) - } - return v.Interface() -} - -var errorType = reflect.TypeFor[error]() - -// onlyCancellation reports whether err says nothing beyond context.Canceled: -// the cancellation itself, or wrappings of it. A worker that returns -// errors.Join(ctx.Err(), failure) after being cancelled is reporting the -// failure, and errors.Is would have called the whole thing a cancellation. -func onlyCancellation(err error) bool { - if err == context.Canceled { - return true - } - switch u := err.(type) { - case interface{ Unwrap() []error }: - errs := u.Unwrap() - if len(errs) == 0 { - return false - } - for _, e := range errs { - if !onlyCancellation(e) { - return false - } - } - return true - case interface{ Unwrap() error }: - inner := u.Unwrap() - return inner != nil && onlyCancellation(inner) - } - return false -} - -func (b Binding[T]) edit(f func(*binding)) Binding[T] { - b.s.mu.Lock() - defer b.s.mu.Unlock() - if b.s.frozen && !slices.Contains(b.s.pending, b.b) { - panic(fmt.Sprintf("di: %s (provided at %s) modified after the scope was first resolved", b.b.key, b.b.where())) - } - f(b.b) - return b -} - -// Group makes the binding a member of the multi-binding group for T instead -// of the binding for T: it neither shadows nor is shadowed by another -// registration of T, and the members are read back together with s.All[T](). -// A member keeps its own lifetime and hooks. -func (b Binding[T]) Group() Binding[T] { - return b.edit(func(b *binding) { b.group = true }) -} - -// Override declares that this registration replaces an earlier one of the same -// key in the same scope. Without it a second registration of a key is rejected -// at the next resolution, naming both sites, because a duplicate that wins -// silently is how one module reroutes another module's wiring without anyone -// noticing. With it the later registration serves the key, and inherits its -// eagerness, which is the test seam: -// -// s := di.Test(t, app.Production) -// s.Value(&DB{DSN: "sqlite://memory"}).Override() -// -// There must be something to override in this scope, or that is rejected too: -// a fake for a service that has since been renamed would otherwise be a -// registration nobody resolves, and the test would pass against production -// wiring. A child scope shadows its parent without Override, since that is a -// different registry rather than a replacement. A key that has already served -// a value cannot be overridden at all. -func (b Binding[T]) Override() Binding[T] { - return b.edit(func(b *binding) { b.override = true }) -} - -// Scoped makes the binding one-per-scope: each scope that resolves it gets -// its own instance, built in that scope (so it can see that scope's -// values) and stopped with it. Declare request-scoped services once in the -// root and resolve them through the request scope. -func (b Binding[T]) Scoped() Binding[T] { - return b.edit(func(b *binding) { b.scoped = true }) -} - -// Eager builds the service during Start rather than on first use. -// -// Eagerness belongs to the key, not the registration: it means the service -// exists by the time Start returns. Overriding an eager binding therefore -// keeps the key eager and builds the replacement; a replacement with a -// per-scope lifetime, which cannot be built once at Start, is rejected. -func (b Binding[T]) Eager() Binding[T] { return b.edit(func(b *binding) { b.eager = true }) } - -// Typed lifecycle hooks: no interface sniffing, no reflection. -// -// OnStart runs once the service is built, and only a hook that returns -// normally starts it: one that panics fails the start step, like a panicking -// constructor, and the service is never served. -func (b Binding[T]) OnStart(f func(context.Context, T) error) Binding[T] { - return b.edit(func(b *binding) { b.onStart = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) -} - -// OnDrain runs before anything is stopped: Stop drains the whole tree, from -// the innermost scope outwards and in reverse build order, while every scope -// still resolves normally. It is where a service stops accepting new work and -// waits for the work it already has, such as an HTTP server that must finish -// in-flight requests whose handlers still need their request scope. Anything -// those handlers build, including a request scope of their own, is drained -// before the phase ends. Use OnStop for the release that follows. -func (b Binding[T]) OnDrain(f func(context.Context, T) error) Binding[T] { - return b.edit(func(b *binding) { b.onDrain = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) -} -func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T] { - return b.edit(func(b *binding) { b.onStop = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) -} - -// Worker registers a long-running function for T, such as a consumer loop. It is -// started in its own goroutine once the service starts and its context is -// cancelled when the service stops; Stop waits for it to return, bounded by -// its own context. A hook that outlasts that deadline is reported by Stop, -// and OnStop then waits for it rather than releasing the value underneath a -// worker still reading it. -// -// Returning a non-nil error calls Shutdown with it, stopping the application, -// even if the scope was already stopping: a worker may fail, flush while the -// scope winds down, and only then report. The exception is context.Canceled -// from a hook that was already cancelled, which is a worker reporting the -// cancellation and nothing else. A hook that wants to stay quiet during -// shutdown should return nil. -func (b Binding[T]) Worker(f func(context.Context, T) error) Binding[T] { - return b.edit(func(b *binding) { b.worker = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) -} - -// ---- resolution ------------------------------------------------------------ - -// as unwraps a stored value. A nil interface is a legitimate service, and a -// nil any cannot be asserted back to the interface type it was stored as, so -// it becomes T's zero value rather than a panic. Every hand-back of a stored -// value goes through here. -func as[T any](v any) T { - if v == nil { - var zero T - return zero - } - return v.(T) -} - -// lookup finds the binding registered for k in this scope or an ancestor, -// and the scope that owns it. A nil binding means nothing is registered for -// k anywhere in the chain. -func (s *Scope) lookup(k key) (*binding, *state) { - for st := s.state; st != nil; st = st.parent { - st.freeze() - st.mu.Lock() - b, ok := st.index[k] - st.mu.Unlock() - if ok { - return b, st - } - } - return nil, nil -} - -// inFlight reports whether this Scope is a live view of a resolution: it -// carries a path whose last node has not returned. A Scope kept past that -// point, by a constructor or by a Child made in one, is not in flight, and -// calls through it are top-level calls. -func (s *Scope) inFlight() bool { return s.r != nil && !s.r.done.Load() } - -// enter returns a view carrying a resolver, starting a new resolution unless -// one is in flight. -func (s *Scope) enter() *Scope { - if s.inFlight() { - return s - } - return s.view(&resolver{}) -} - -// get resolves k. Outside a constructor the internal abort is converted into -// a panic carrying the plain error; inside one it unwinds to the enclosing -// Resolve/Start call. -func (s *Scope) get(k key) any { - if !s.inFlight() { - defer unwrapAbort() - return s.enter().get(k) - } - b, owner := s.lookup(k) - if b == nil { - panic(abort{fmt.Errorf("di: %s: %w%s", k, ErrNotProvided, s.r.path())}) - } - v := s.resolve(b, owner) - s.markServed(owner, k) - return v -} - -// markServed records that k was served to this scope from owner, in every -// scope between the two. binding.used protects the owner; the scopes in -// between each handed out a value for k as well, and registering k in one of -// them afterwards would give the key two live values there. -func (s *Scope) markServed(owner *state, k key) { - for st := s.state; st != nil && st != owner; st = st.parent { - st.mu.Lock() - if st.served == nil { - st.served = make(map[key]bool, 4) - } - st.served[k] = true - st.mu.Unlock() - } -} - -// resolve produces b's value for the resolving scope s, honouring the -// binding's lifetime and starting the instance when the scope is running. -func (s *Scope) resolve(b *binding, owner *state) any { - if s.isStopped() { - panic(abort{fmt.Errorf("di: %s: %w%s", b.key, ErrStopped, s.r.path())}) - } - // The holder owns the instance's lifecycle: a singleton lives in the - // scope that registered the binding, a scoped one in the scope that - // resolves it, so it can see that scope's values. - holder := owner - if b.scoped { - holder = s.state - } - if s.r.onPath(b, holder) { - panic(abort{fmt.Errorf("di: %w: %s -> %s", ErrCycle, s.r.pathStr(), b.key)}) - } - if !b.used.Load() { - // Hold the key against an override for as long as this resolution - // runs, so a constructor cannot replace the registration it is - // itself being built from. used is set before this is dropped, so - // the two guards never leave a gap between them. - b.resolving.Add(1) - defer b.resolving.Add(-1) - } - sc := s.view(s.r.child(b, holder)) - // The node stops being a dependency when this resolution returns, however - // it returns. See resolver.done. - defer sc.r.done.Store(true) - - in := holder.instanceFor(b) - v, err := sc.await(in, holder) - if err != nil { - panic(abort{err}) - } - b.used.Store(true) - // The edge belongs to whoever asked, which is the node this resolution - // hangs off, not the one it just made. A failed resolution records - // nothing: it built no value, and the path it failed on is in the error. - // - // The test is here rather than inside dependOn so that a resolution with - // nobody to tell pays a pointer comparison instead of a call: a node with - // no binding is a top-level Get, which is the warm path. - if s.r.b != nil { - s.r.dependOn(in, holder) - } - return v -} - -// instanceFor picks the instance a resolution uses. A singleton has one for -// the whole binding; a Scoped binding has one per scope that holds it. -func (st *state) instanceFor(b *binding) *instance { - if !b.scoped { - return b.single - } - st.mu.Lock() - defer st.mu.Unlock() - in := st.scoped[b] - if in == nil { - in = &instance{b: b} - st.scoped[b] = in - } - return in -} - -// instanceAt is instanceFor without the making: the instance b already has -// in st, or nil. Called with st's mutex held, by the callers that must not -// bring one into being -- a recorded edge, and the inspection API. -func (st *state) instanceAt(b *binding) *instance { - if !b.scoped { - return b.single - } - return st.scoped[b] -} - -// dependOn records that the resolution at this node needed in. The node is -// the one that asked, so the edge lands on its instance; the caller checks -// first that there is one, since a node with no binding is a top-level call -// and owes nothing to anybody. That is what keeps this off the warm path: a -// Get made outside a constructor starts a fresh path whose first node has no -// binding, and a Scope kept past its resolution is no longer in flight, so it -// starts one too. -// -// Recorded once per distinct dependency. A constructor that asks for the -// same service twice, or resolves in a loop, gets one edge; the scan that -// buys that is over one constructor's own dependencies and runs while it is -// building, never on a resolution that is only reading a built value. -func (r *resolver) dependOn(in *instance, holder *state) { - r.holder.mu.Lock() - defer r.holder.mu.Unlock() - // The asking instance is being built, so it exists: resolve made it - // before it ran the constructor this call came from. - asker := r.holder.instanceAt(r.b) - if asker == nil || slices.ContainsFunc(asker.deps, func(d dep) bool { return d.in == in }) { - return - } - asker.deps = append(asker.deps, dep{in: in, holder: holder}) -} - -// await returns the instance's value: this branch builds it if it gets there -// first, and otherwise waits for whoever did. It waits for the start step as -// well, so a resolution of a running scope never hands out a service whose -// OnStart is still in flight. A wait that would close a cycle between two -// concurrent builds is reported as ErrCycle rather than deadlocking. -func (s *Scope) await(in *instance, holder *state) (any, error) { - holder.mu.Lock() - for in.ph == phaseNew || !in.settled || in.ph == phaseStarting { - if in.ph == phaseNew { - in.claimBuild(holder, s.r) - holder.mu.Unlock() - s.materialise(in, holder) - holder.mu.Lock() - continue - } - // The phase says which step is outstanding and so which channel to - // block on. Both are read in this critical section, and the owner - // closes the channel under the same mutex, so it cannot be closed - // between the choice and the block. - var ready chan struct{} - if in.settled { - ready = waitOn(&in.startingCh) // settled, so OnStart is outstanding - } else { - ready = waitOn(&in.settledCh) - } - if !s.r.wait(holder.graph, in) { - holder.mu.Unlock() - return nil, fmt.Errorf("di: %w: %s -> %s", ErrCycle, s.r.parent.pathStr(), in.b.key) - } - holder.mu.Unlock() - <-ready - s.r.unwait(holder.graph) - holder.mu.Lock() - } - value, err := in.value, in.err - if err == nil && s.isStopped() { - // The scope stopped while this branch was building or waiting; - // resolve's check was before the wait. The check is on the resolving - // scope, which covers the holder (always that scope or an ancestor): - // a stopped scope must refuse the request whether or not the value is - // still alive above it. - value, err = nil, fmt.Errorf("di: %s: %w", in.b.key, ErrStopped) - } - holder.mu.Unlock() - return value, err -} - -// materialise builds an instance, once. A failure is recorded on the instance -// rather than unwound, so every later resolution reports it identically, and -// the instance is settled on the way out so waiters are released whatever -// happened. -func (s *Scope) materialise(in *instance, holder *state) { - defer in.settle(holder) - if err := s.construct(in, holder); err != nil { - in.fail(holder, err) - return - } - if !in.publish(holder) { - return - } - in.startIfRunning(holder) -} - -// construct runs the constructor, turning a panic or an abort from a nested -// resolution into an error, and reports the attempt to observers either way. -func (s *Scope) construct(in *instance, holder *state) (err error) { - b := in.b - t0 := time.Now() - defer func() { - if rec := recover(); rec != nil { - if a, ok := rec.(abort); ok { - err = fmt.Errorf("di: building %s (provided at %s): %w", b.key, b.where(), a.err) - } else { - err = fmt.Errorf("di: building %s (provided at %s): panic: %v", b.key, b.where(), rec) - } - } - holder.emit(Event{Kind: EventBuild, Service: b.key.String(), Package: b.key.pkgPath(), Scope: holder.name, Site: b.site, Module: b.module, Duration: time.Since(t0), Err: err}) - }() - in.value = b.build((&Scope{state: holder, module: b.module}).view(s.r)) - return nil -} - -// publish adds the instance to its owner's stop list so it will be torn -// down. If Stop ran while the constructor was in flight its snapshot did not -// include this instance, so undo it here and report ErrStopped instead. -func (in *instance) publish(owner *state) bool { - owner.mu.Lock() - stopped := owner.isStopped() - in.ph = phaseBuilt - if !stopped { - owner.started = append(owner.started, in) - } - owner.mu.Unlock() - if !stopped { - return true - } - err := errors.Join(fmt.Errorf("di: %s: %w", in.b.key, ErrStopped), in.stopIfNeeded(owner.stopContext(), owner)) - owner.mu.Lock() - in.err = err - owner.mu.Unlock() - return false -} - -// startIfRunning runs the start step when the scope is already running. -// publish strictly precedes the read below, and Start sets running before it -// drains, so either this starts the instance or Start's drain finds it. -// startClaimed records its own failure on the instance, so a resolution that -// waited for the step reports it too. -func (in *instance) startIfRunning(owner *state) { - if sctx, running := owner.runContext(); running && in.claim(owner) { - if sctx == nil { - sctx = context.Background() - } - _ = in.startClaimed(sctx, owner) - } - stopped := owner.isStopped() - owner.mu.Lock() - if in.err == nil && stopped { - // Stop ran while we were starting; it waits for the start step, so - // the instance is torn down. Do not hand it out. - in.err = fmt.Errorf("di: %s: %w", in.b.key, ErrStopped) - } - owner.mu.Unlock() -} - -func (st *state) isStopped() bool { - for ; st != nil; st = st.parent { - if st.stopped.Load() { - return true - } - } - return false -} - -// Get resolves T. Inside a constructor, failure unwinds to the enclosing -// Resolve/Start call and becomes an error; at top level it panics. In a -// goroutine a constructor started, use Resolve instead: that panic has no -// enclosing call to unwind to and would take the process down. -func (s *Scope) Get[T any]() T { return as[T](s.get(key{t: reflect.TypeFor[T]()})) } - -// Maybe resolves T if it is provided anywhere in the scope chain. -func (s *Scope) Maybe[T any]() (T, bool) { - if b, _ := s.lookup(key{t: reflect.TypeFor[T]()}); b == nil { - var zero T - return zero, false - } - return s.Get[T](), true -} - -// All resolves the multi-binding group for T across the scope chain. Members -// are singletons (or Scoped if so marked) with the same lifecycle -// as any other binding. -func (s *Scope) All[T any]() []T { - if !s.inFlight() { - defer unwrapAbort() - return s.enter().All[T]() - } - k := key{t: reflect.TypeFor[T]()} - var out []T - for st := s.state; st != nil; st = st.parent { - st.freeze() - st.mu.Lock() - bs := slices.Clone(st.groups[k]) - st.mu.Unlock() - for _, b := range bs { - out = append(out, as[T](s.resolve(b, st))) - } - } - return out -} - -// Must unwraps a (value, error) pair inside a constructor: -// -// db := s.Must(sql.Open("postgres", dsn)) -// -// A non-nil error aborts the constructor and surfaces from the enclosing -// Resolve, Start or Run. Outside a constructor it panics with the error. -func (s *Scope) Must[T any](v T, err error) T { - if err == nil { - return v - } - if s.inFlight() { - panic(abort{err}) - } - panic(err) -} - -// Context returns the context passed to Start (or Run) on this scope or the -// nearest started ancestor, so constructors can dial with a deadline. Before -// Start it returns context.Background(). -func (s *Scope) Context() context.Context { - if ctx, _ := s.runContext(); ctx != nil { - return ctx - } - return context.Background() -} - -// runContext walks up the scope chain to the nearest state that Start was -// called on. running reports whether that Start has passed its hook phase, -// which is when bindings built later must start themselves. -func (st *state) runContext() (ctx context.Context, running bool) { - for ; st != nil; st = st.parent { - st.mu.Lock() - ctx, running = st.startCtx, st.running - st.mu.Unlock() - if ctx != nil { - return ctx, running - } - } - return nil, false -} - -// Resolve is the error-returning entry point. -func (s *Scope) Resolve[T any]() (v T, err error) { - defer recoverAbort(&err) - return as[T](s.enter().get(key{t: reflect.TypeFor[T]()})), nil -} - -// unwrapAbort turns the internal abort panic into a panic carrying the plain -// error, which is what a top-level Get or All reports. Deferred by the entry -// points that are not already inside a resolution; a call made from a -// constructor lets the abort unwind to the enclosing Resolve or Start -// instead. -func unwrapAbort() { - if rec := recover(); rec != nil { - if a, ok := rec.(abort); ok { - panic(a.err) - } - panic(rec) - } -} - -func recoverAbort(err *error) { - if rec := recover(); rec != nil { - if a, ok := rec.(abort); ok { - *err = a.err - return - } - panic(rec) - } -} - -// ---- lifecycle ------------------------------------------------------------- - -// Start builds every Eager binding in registration order, then runs the -// start step of everything built so far, in build order. If a constructor -// or a start step fails, the scope is stopped, which rolls back exactly the -// services that did start, child scopes included. A service that was built -// but never started is not stopped, so acquire resources in OnStart rather -// than in the constructor when the binding declares one. -// -// After Start returns, a service built later runs its start step as part of -// being built, so lazily resolved services start too. Start may be called -// once, and builds only this scope's own Eager bindings: a child scope's are -// built by that child's Start. -func (s *Scope) Start(ctx context.Context) error { - // Start has no deadline of its own for the rollback, so it detaches the - // caller's context: an already-cancelled ctx must not skip the teardown. - return s.start(ctx, func() (context.Context, func()) { - return context.WithoutCancel(ctx), func() {} +// report emits the event for one lifecycle step of b, owned by this scope, +// that began at t0 and ended with err. +func (st *state) report(kind EventKind, b *binding, t0 time.Time, err error) { + st.emit(Event{ + Kind: kind, Service: b.key.String(), Package: b.key.pkgPath(), + Scope: st.name, Site: b.site, Module: b.module, + Duration: time.Since(t0), Err: err, }) } -// start is Start with the rollback context supplied by the caller: Start -// detaches the caller's context, Run applies its StopTimeout and signal -// handling. -func (s *Scope) start(ctx context.Context, rollbackCtx func() (context.Context, func())) (err error) { - defer recoverAbort(&err) - s.freeze() - s.mu.Lock() - if s.startCtx != nil { - s.mu.Unlock() - return errors.New("di: Start called twice") - } - s.startCtx = ctx - eager := slices.Clone(s.eager) // derived at freeze; clone so a later freeze cannot truncate it - s.mu.Unlock() - - // A failing eager constructor must roll back like a failing hook. - if err := s.buildEager(eager); err != nil { - return errors.Join(err, s.rollback(rollbackCtx)) - } - - s.mu.Lock() - s.running = true - s.mu.Unlock() - - // Drain: anything built before the flag was set is still waiting here, - // and starting one service may build more. - for { - in, owner := s.claimNext() - if in == nil { - if s.isStopped() { - // A start hook stopped the scope; nothing is running. - return fmt.Errorf("di: Start: %w", ErrStopped) - } - return nil - } - if err := in.startClaimed(ctx, owner); err != nil { - err = fmt.Errorf("di: starting %s: %w", in.b.key, err) - return errors.Join(err, s.rollback(rollbackCtx)) - } - } -} - -func (s *Scope) rollback(mk func() (context.Context, func())) error { - ctx, cancel := mk() - defer cancel() - return s.Stop(ctx) -} - -// buildEager builds the eager bindings, turning a constructor failure into -// an error rather than letting it unwind past Start's rollback. -func (s *Scope) buildEager(eager []*binding) (err error) { - defer recoverAbort(&err) - for _, b := range eager { - if b.group { - // A group member is not reachable by key: resolve it directly. - s.enter().resolve(b, s.state) - continue - } - // By key, so whichever registration owns the key is what gets built; - // deriveEager has already checked that it can honour eagerness. - s.enter().get(b.key) - } - return nil -} - -// claimNext claims the start step of the next built-but-unstarted instance -// in this scope or a descendant, in build order. -func (st *state) claimNext() (*instance, *state) { - st.mu.Lock() - for _, in := range st.started { - if in.ph == phaseBuilt { - in.ph = phaseStarting - st.mu.Unlock() - return in, st - } - } - children := slices.Clone(st.children) - st.mu.Unlock() - for _, c := range children { - if in, owner := c.claimNext(); in != nil { - return in, owner - } - } - return nil, nil -} - -// Stop winds the scope down in three phases. First it drains: OnDrain hooks -// run from the innermost scope outwards, in reverse build order, while every -// scope still resolves, so work already in flight can finish and still reach -// its dependencies. A service or child scope that phase brings into being is -// drained too, before anything is marked stopped. Then the scope is marked -// stopped and child scopes are stopped. Then OnStop hooks run in reverse -// build order (dependents first). -// -// A service is stopped only if it started, or if it declares no OnStart, in -// which case OnStop is a plain destructor. Every failure is reported. -// -// Stop is synchronous. It waits out whatever another goroutine is still -// running for a service it is tearing down -- a start step in flight, a drain -// hook another Stop began, a Worker hook being cancelled -- so when it returns, -// the teardown has happened and its failures are in the error. A teardown -// outlives the call in one case, when ctx expires first: the missed deadline -// is reported here, and the release is finished once the outstanding step -// returns, on a goroutine of its own, reaching observers rather than this -// caller. A constructor that finishes after the scope has stopped is likewise -// undone by whichever goroutine was resolving it. -// -// Afterwards the scope and its descendants refuse to resolve anything, with -// ErrStopped; that includes a resolution that was already waiting when the -// scope stopped, so a closed service is never handed out. Stopping a child -// scope also detaches it from its parent, so per-request scopes are released -// once stopped. -// -// Stop is idempotent, and concurrent calls are safe: only the first tears the -// scope down, and the others wait for it and report its result, bounded by -// their own context. Two Stop calls that meet at one scope, as a child and -// its parent do, wait for each other phase by phase, so neither starts -// releasing what the other's hooks are still using. -// -// Because Stop waits, a hook must not call Stop on its own scope or an -// ancestor: it would be waiting for the step it is itself running. Stopping a -// sibling, or a scope below the hook's own, is allowed. A hook that passes on -// the context it was given gets an error saying so; one that passes a context -// of its own is not recognised, and waits until that context expires. Call -// Shutdown, which never blocks. -func (s *Scope) Stop(ctx context.Context) error { - // A hook of this scope, or of one under it, calling Stop here would be - // waiting for a step it is itself running. Report it: the wait would - // otherwise last until ctx expired, and with a background context for - // ever. Only a hook that passed on the context it was given can be seen - // this way, which is the shape worth catching. - if h := hookOwner(ctx); h != nil && h.descendsFrom(s.state) { - return fmt.Errorf("di: a lifecycle hook of scope %s called Stop on scope %s, which it is inside: call Shutdown instead", h.name, s.name) - } - if !s.stopOnce.claim(s.state, func() { - if s.stopCtx == nil { - s.stopCtx = ctx // the first Stop owns it; a later call must not clobber it - } - }) { - err, finished := s.stopOnce.wait(s.state, ctx) - if !finished { - return fmt.Errorf("di: waiting for scope %s to stop: %w", s.name, ctx.Err()) - } - return err // the owning teardown's result, reported to this call too - } - err := s.teardown(ctx) - s.stopOnce.settle(s.state, err) - return err -} - -// teardown is the body of the first Stop. -func (s *Scope) teardown(ctx context.Context) error { - errs := []error{s.drain(ctx)} - - s.mu.Lock() - children := slices.Clone(s.children) - started := s.started - s.started = nil - s.stopped.Store(true) - s.mu.Unlock() - - for _, c := range children { - errs = append(errs, (&Scope{state: c}).Stop(ctx)) - } - errs = append(errs, stopAll(ctx, s.state, started)) - - if p := s.parent; p != nil { - p.mu.Lock() - p.children = slices.DeleteFunc(p.children, func(c *state) bool { return c == s.state }) - p.mu.Unlock() - } - return errors.Join(errs...) -} - -// drain runs the OnDrain hooks of this scope's subtree before anything is -// marked stopped, innermost first and in reverse build order, the order Stop -// uses. Nothing here changes an instance's phase. -// -// Only the first drain of a scope runs; a Stop that reaches the scope by -// another route waits for it. Without that wait, when a child and its parent -// are stopped at once, the second Stop would walk past a drain still in -// flight and start releasing what its hooks are using. -func (s *Scope) drain(ctx context.Context) error { - if !s.drainOnce.claim(s.state, nil) { - // Whoever owns the phase settles it with what this scope's own hooks - // reported, and that answer belongs to this call too: a Stop must - // report the failure of a drain hook of its own scope whether it ran - // the hook or waited for someone else to. Reporting it twice inside - // one aggregate is what the run avoids, by keeping each scope's - // errors out of its own return value and letting them arrive through - // that scope's Stop. - err, finished := s.drainOnce.wait(s.state, ctx) - if !finished { - return fmt.Errorf("di: waiting for scope %s to drain: %w", s.name, ctx.Err()) - } - return err - } - root := &drainScope{st: s.state, ours: true} - r := drainRun{root: root, seen: map[*state]*drainScope{s.state: root}} - err := r.sweepAll(ctx) - s.drainOnce.settle(s.state, err) // this scope's phase is the last to end - return err -} - -// drainRun is the bookkeeping of one drain phase: the scopes it has reached, -// whether it owns each one's phase, and whether that phase has ended. -type drainRun struct { - root *drainScope - seen map[*state]*drainScope -} - -type drainScope struct { - st *state - ours bool // this run claimed the phase; otherwise another Stop owns it - settled bool // its phase has ended; for a descendant, when its own sweep does -} - -// sweepAll is the body of the first drain. It sweeps the subtree until a pass -// finds no new work, because the scope still resolves during this phase: a -// hook finishing in-flight work may build a service or open a child scope, -// and those owe a drain too, before anything is marked stopped. ctx bounds -// the sweep as well as the hooks, so a hook that keeps building cannot hold -// the phase open for ever. -func (r *drainRun) sweepAll(ctx context.Context) error { - var errs []error - for { - progress := false - errs = append(errs, r.visit(ctx, r.root, &progress)...) - if !progress || ctx.Err() != nil { - return errors.Join(errs...) - } - } -} - -// visit sweeps one scope this run owns and everything below it, innermost -// first and in reverse creation order, the order Stop uses. Every owned scope -// is swept on every pass, not only the ones that appeared in it, because a -// hook may build into a scope already visited. -// -// It returns the errors that belong to *this* scope's Stop. A descendant's go -// into that descendant's phase instead, so its own Stop reports them -- the -// one that a request handler is holding, say, and not only the application -// Stop that happened to run the hook. They reach this caller anyway, because -// teardown stops its children and joins what their Stop returns, and that is -// the route that keeps one failure to one place in the aggregate. Errors -// found in a descendant after its phase has ended have nowhere to be settled, -// so those bubble up here instead of being dropped. -// -// A descendant's phase is claimed just before its subtree is swept and ended -// as soon as that sweep finishes, so while a hook runs the only unended phases -// this run holds are the scope being swept and its ancestors, which a hook may -// not Stop anyway. Claiming the whole subtree up front would deadlock a hook -// that stops a scope the walk has claimed but not yet reached, such as a -// server draining in one child and stopping a request scope in another. -// -// A scope another Stop already owns is waited for and then left alone, -// subtree included; that Stop's run drains it. -func (r *drainRun) visit(ctx context.Context, ds *drainScope, progress *bool) []error { - var errs []error - ds.st.mu.Lock() - children := slices.Clone(ds.st.children) - ds.st.mu.Unlock() - for _, c := range slices.Backward(children) { - cs := r.seen[c] - if cs == nil { - *progress = true - cs = &drainScope{st: c} - r.seen[c] = cs - if c.drainOnce.claim(c, nil) { - cs.ours = true - } else if _, finished := c.drainOnce.wait(c, ctx); !finished { - errs = append(errs, fmt.Errorf("di: waiting for scope %s to drain: %w", c.name, ctx.Err())) - } - } - if cs.ours { - // Whatever comes back could not be settled into the child's own - // phase, so it belongs to this scope's aggregate. - errs = append(errs, r.visit(ctx, cs, progress)...) - } - } - ds.st.mu.Lock() - started := slices.Clone(ds.st.started) - ds.st.mu.Unlock() - for _, in := range slices.Backward(started) { - ran, err := in.drainIfNeeded(ctx, ds.st) - *progress = *progress || ran - errs = append(errs, err) - } - if ds != r.root && !ds.settled { - ds.st.drainOnce.settle(ds.st, errors.Join(errs...)) - ds.settled = true - return nil // reported by this scope's own Stop, not by its parent's - } - return errs -} - -func stopAll(ctx context.Context, owner *state, started []*instance) error { - var errs []error - for _, in := range slices.Backward(started) { - errs = append(errs, in.stopIfNeeded(ctx, owner)) - } - return errors.Join(errs...) -} - -// ---- testing --------------------------------------------------------------- - // TB is the subset of testing.TB that Test needs. type TB interface { Helper() @@ -2316,8 +303,6 @@ func Test(tb TB, wire ...Module) *Scope { return s } -// ---- request scopes -------------------------------------------------------- - type ctxKey struct{} // WithScope attaches s to ctx so handlers and their callees can reach it @@ -2331,117 +316,3 @@ func FromContext(ctx context.Context) (*Scope, bool) { s, ok := ctx.Value(ctxKey{}).(*Scope) return s, ok } - -// Shutdown asks a running Run to stop and records the cause it should return. -// It never blocks, may be called from any goroutine, and the first call wins. -// It propagates to ancestor scopes, so a service in a child scope can stop the -// application. -func (s *Scope) Shutdown(cause error) { - first := false - for st := s.state; st != nil; st = st.parent { - st.shutdownOnce.Do(func() { - st.shutdownErr = cause - close(st.shutdownCh) - first = first || st == s.state - }) - } - if first { - s.emit(Event{Kind: EventShutdown, Scope: s.name, Err: cause}) - } -} - -// RunOption configures Run. -type RunOption func(*runConfig) - -type runConfig struct{ stopTimeout time.Duration } - -// exitSignals are what make Run exit: an interrupt or a termination request. -var exitSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} - -// StopTimeout bounds how long Stop may take once Run decides to exit. -// The default is 15 seconds. -func StopTimeout(d time.Duration) RunOption { return func(c *runConfig) { c.stopTimeout = d } } - -// stopContext builds the context Run stops with: detached from the caller's, -// bounded by StopTimeout, and cancelled by a second signal so a hung hook -// cannot keep the process alive. A rollback from a failed Start gets the same -// context. -func (c runConfig) stopContext(ctx context.Context) (context.Context, func()) { - stopCtx, cancelStop := context.WithTimeout(context.WithoutCancel(ctx), c.stopTimeout) - forceCtx, cancelForce := signal.NotifyContext(stopCtx, exitSignals...) - return forceCtx, func() { cancelForce(); cancelStop() } -} - -// Run starts the scope and blocks until ctx is cancelled, a termination -// signal arrives, or Shutdown is called. It then stops the scope with a -// bounded context; a second signal during the stop cancels that context so -// a hung hook cannot keep the process alive. Run returns the Start error, the -// error passed to Shutdown, and any Stop errors, joined. A worker that died -// on its own is reported once, whether it reached Run as the cause or as a -// Stop error. -func (s *Scope) Run(ctx context.Context, opts ...RunOption) error { - cfg := runConfig{stopTimeout: 15 * time.Second} - for _, o := range opts { - o(&cfg) - } - - // Register before Start so a signal during a slow start is not lost. - sigCtx, cancelSig := signal.NotifyContext(ctx, exitSignals...) - defer cancelSig() - - if err := s.start(ctx, func() (context.Context, func()) { return cfg.stopContext(ctx) }); err != nil { - // A failed Start rolls back through Stop, which runs the drain and - // stop hooks -- so a worker can die and publish its failure here - // exactly as it can during an ordinary shutdown. This path used to - // return before the cause was ever read. - return joinCause(err, s.publishedCause()) - } - - var cause error - select { - case <-sigCtx.Done(): - case <-s.shutdownCh: - cause = s.shutdownErr - } - - stopCtx, cancel := cfg.stopContext(ctx) - defer cancel() - - stopErr := s.Stop(stopCtx) - if cause == nil { - // A worker that died during the stop published its failure through - // Shutdown after the select above had woken for a signal or a - // cancelled ctx. Read it again: the Stop that saw the failure may have - // been a child's, called from a drain hook that handled the error - // itself. - cause = s.publishedCause() - } - return joinCause(stopErr, cause) -} - -// publishedCause reports the failure Shutdown recorded, without waiting for -// one. Run reads it on both ways out, because a worker dies when it dies: -// during the stop that follows a signal, or during the rollback of a Start -// that never finished. -func (s *Scope) publishedCause() error { - select { - case <-s.shutdownCh: - return s.shutdownErr - default: - return nil - } -} - -// joinCause adds a published cause to what Run is already returning, unless -// that failure is in there already: a worker's error reaches Run by two -// routes, as the cause and through the Stop that cancelled it, and it is one -// failure either way. -func joinCause(err, cause error) error { - if cause == nil { - return err - } - if errors.Is(err, cause) { - return err // one failure, reached by both routes - } - return errors.Join(err, cause) -} diff --git a/docs/DESIGN.md b/docs/DESIGN.md index eb74d94..4deb049 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -374,7 +374,12 @@ scope stopped is undone the same way. | File | What it holds | |---|---| -| [`di.go`](../di.go) | registration, resolution, the phase machine, scopes, lifecycle | +| [`di.go`](../di.go) | the package doc, keys, events, `Scope`, modules, `Test` | +| [`binding.go`](../binding.go) | registration: `Provide`, `Value`, `Wire`, `Wrap` and the `Binding` handle | +| [`state.go`](../state.go) | a scope's registry, `freeze`, the readers that walk the parent chain | +| [`resolve.go`](../resolve.go) | the resolution path, both cycle detectors, the build step, `Get` and friends | +| [`lifecycle.go`](../lifecycle.go) | the phase machine, the hooks, `Start` and `Stop` | +| [`run.go`](../run.go) | `Run` and `Shutdown` | | [`validate.go`](../validate.go) | the walk over declared dependencies | | [`explain.go`](../explain.go) | the two renderings of the graph | | [`dihttp/`](../dihttp) | the net/http adapter: request scopes and handlers | diff --git a/explain.go b/explain.go index 0ead2c6..c9ad96f 100644 --- a/explain.go +++ b/explain.go @@ -1,7 +1,7 @@ package di // Rendering the graph. Nothing here participates in resolution or teardown: -// it reads the edges di.go records while constructors run, under the same +// it reads the edges resolve.go records while constructors run, under the same // mutex that guards every other field of an instance, and never holds two of // those at once, and it reads the dependency lists Wire declares, which never // change after registration. It is also the only part of the package that diff --git a/lifecycle.go b/lifecycle.go new file mode 100644 index 0000000..db9a71e --- /dev/null +++ b/lifecycle.go @@ -0,0 +1,820 @@ +package di + +// The lifecycle: an instance's phase machine, the hooks that move it through +// its start, drain and stop steps, and Start and Stop, which drive that +// machine for a whole scope tree. Every phase is read and written under the +// owning state's mutex, and every user hook is called through callHook. + +import ( + "context" + "errors" + "fmt" + "slices" + "time" +) + +// phase is an instance's position in the build/start/stop sequence. It is +// read and written only under the owning state's mutex, so deciding who +// starts or stops an instance never spans two critical sections. +type phase int8 + +const ( + phaseNew phase = iota // no value yet + phaseBuilding // a resolution has claimed the build step + phaseBuilt // constructor ran; the start step has not + phaseStarting // a goroutine has claimed the start step + phaseStarted // the start step succeeded + phaseFailed // the build or the start step failed + phaseStopped // the stop step ran, or was skipped for good +) + +// drainPhase tracks OnDrain the way phase tracks the build and start steps: +// a drain in progress is waited for, so a waiter has to tell it from one that +// has finished. +type drainPhase int8 + +const ( + drainNone drainPhase = iota // OnDrain has not been considered + draining // a Stop is running OnDrain now + drained // OnDrain ran, or was skipped for good +) + +// dep is one recorded dependency edge: an instance a constructor resolved, +// and the scope holding it, which is what names it in a rendering. The +// holder is carried rather than looked up because an instance does not know +// its own scope, and the scope it was resolved from may be gone by the time +// anything reads the edge. +type dep struct { + in *instance + holder *state +} + +// instance is one built value of a binding, owned by the state that stops it. +type instance struct { + b *binding + ph phase // guarded by the owning state's mutex + value any + err error // guarded by the owning state's mutex + // settled is set when the build step has finished and value and err are + // final. + settled bool // guarded by the owning state's mutex + dr drainPhase // guarded by the owning state's mutex + + // deps are the services this instance's constructor resolved, in the + // order it asked for them, each recorded once however many times it + // asked. Guarded by the owning state's mutex, because a constructor may + // resolve from several goroutines at once and they share the Scope it + // was handed. Only Explain and Graph read them; nothing in the + // build/start/stop machine does. + deps []dep + + // Each step another goroutine may have to wait for has a channel that is + // closed when the step is done, so a waiter blocks on that step alone. + // The first goroutine that has to wait makes the channel (waitOn); the + // owner of the step closes it if it exists (wake). Both happen under the + // owning state's mutex, in the same critical section as the phase change, + // so either order is safe: a waiter that arrives first is released by the + // close, and an owner that finishes first leaves nil behind, in which case + // the phase already says the step is done and the waiter never blocks. + // + // Nil is the normal state. An uncontended build, an unraced start step + // and an undisputed drain allocate nothing. Never block on one of these + // fields directly, since a receive from a nil channel blocks for ever; + // go through waitOn. + settledCh chan struct{} // closed by settle: value and err are final + startingCh chan struct{} // closed when the start step is no longer in flight + drainedCh chan struct{} // closed when OnDrain has finished + + // builder is the resolution running the build step, guarded by the + // container graph's mutex. It is the edge that makes a cycle between + // concurrent builds visible. + builder *resolver + + // Worker hook bookkeeping, guarded by the phase machine rather than a mutex. + // cancel and runDone are written by start, on the goroutine that owns the + // start step, and read by stop, which stopIfNeeded reaches only after + // startClaimed has moved the phase past phaseStarting under the owning + // state's mutex; that lock handoff is the happens-before. runErr is written + // by the worker goroutine before it closes runDone and read only after a + // receive from runDone. + cancel context.CancelFunc + runDone chan struct{} + runErr error +} + +// wake closes a step's channel if a waiter made one. +func wake(ch chan struct{}) { + if ch != nil { + close(ch) + } +} + +// waitOn returns a step's channel, making it on first use. Called under the +// owning state's mutex, in the critical section that read the phase. +func waitOn(ch *chan struct{}) chan struct{} { + if *ch == nil { + *ch = make(chan struct{}) + } + return *ch +} + +// once is a teardown phase that runs at most once per scope: the first caller +// runs it, and every later or concurrent caller waits for that run, bounded by +// its own context. Stop and the scope-wide drain are both this shape. +// +// Its fields are guarded by the state's mutex, so claiming the phase and +// recording what the claim decided are one critical section, and no third +// lock joins the ordering rules. +type once struct { + done chan struct{} // made by the claimer, closed once its run has finished + err error // that run's result +} + +// claim reports whether this caller owns the run. The owner must call settle +// exactly once; everyone else calls wait. claimed, if non-nil, runs under the +// mutex in the same critical section that picks the winner, for state a waiter +// must see as soon as it sees the phase claimed. +func (o *once) claim(st *state, claimed func()) bool { + st.mu.Lock() + defer st.mu.Unlock() + if o.done != nil { + return false + } + o.done = make(chan struct{}) + if claimed != nil { + claimed() + } + return true +} + +// settle publishes the run's result and releases the waiters. +func (o *once) settle(st *state, err error) { + st.mu.Lock() + o.err = err + st.mu.Unlock() + close(o.done) +} + +// wait blocks until the owning run has finished and reports its error, or +// reports false if the caller's context expires first. Only a caller whose +// claim returned false may wait: an unclaimed phase has no channel and would +// block until ctx expires. +func (o *once) wait(st *state, ctx context.Context) (finished bool, err error) { + st.mu.Lock() + done := o.done + st.mu.Unlock() + select { + case <-done: + case <-ctx.Done(): + return false, nil + } + st.mu.Lock() + defer st.mu.Unlock() + return true, o.err +} + +// hookKey marks a context as belonging to a lifecycle hook. +type hookKey struct{} + +// inHook tags the context a hook is called with, so a Stop made with that +// context can name the misuse instead of waiting for a step the caller is +// itself running. A hook that passes a context of its own is not seen. +func inHook(ctx context.Context, st *state) context.Context { + return context.WithValue(ctx, hookKey{}, st) +} + +// hookOwner returns the scope whose hook ctx belongs to, or nil. +func hookOwner(ctx context.Context) *state { + st, _ := ctx.Value(hookKey{}).(*state) + return st +} + +// callHook runs a lifecycle hook and reports what it did as an error, a panic +// included. A hook that panics -- or resolves something whose registration is +// rejected, which reaches it as a panic -- must not take the teardown down +// with it: stopOnce would be claimed and never settled, every later Stop +// would wait for it, and every instance behind it would never be released. +func callHook(hook func(context.Context, any) error, ctx context.Context, v any) (err error) { + defer func() { + if rec := recover(); rec != nil { + if a, ok := rec.(abort); ok { + err = a.err // a nested resolution failed; report that cause + } else { + err = fmt.Errorf("panic: %v", rec) + } + } + }() + return hook(ctx, v) +} + +// start runs OnStart and launches the Worker hook. The worker's context is +// detached from ctx so the worker is cancelled by Stop, in dependency order, +// rather than the moment the application context is cancelled. +func (in *instance) start(ctx context.Context, owner *state) error { + b := in.b + if b.onStart != nil { + t0 := time.Now() + err := callHook(b.onStart, inHook(ctx, owner), in.value) + owner.report(EventStart, b, t0, err) + if err != nil { + return err + } + } + if b.worker != nil { + rctx, cancel := context.WithCancel(context.WithoutCancel(ctx)) + in.cancel, in.runDone = cancel, make(chan struct{}) + hctx := inHook(rctx, owner) + go func() { + defer close(in.runDone) + err := b.worker(hctx, in.value) + if err == nil { + return + } + if rctx.Err() != nil && onlyCancellation(err) { + return // we cancelled it and it reported just that + } + // Any other error is the worker's own failure and goes to + // Shutdown, whether or not the scope had begun stopping: + // rctx.Err() says whether we cancelled, not why the worker + // failed. It is wrapped once and kept, so that Stop, which + // reports it, and Run, which receives it, recognise one failure + // rather than listing it twice. + in.runErr = fmt.Errorf("di: %s: %w", b.key, err) + (&Scope{state: owner}).Shutdown(in.runErr) + }() + } + return nil +} + +// claim takes the start step for this goroutine, returning false if another +// one already has it or the instance is past starting. +func (in *instance) claim(owner *state) bool { + owner.mu.Lock() + defer owner.mu.Unlock() + if in.ph != phaseBuilt { + return false + } + in.ph = phaseStarting + return true +} + +// startClaimed runs the start step of an instance already in phaseStarting +// and settles the phase, which releases a Stop or a resolution waiting for +// the step. Only a hook that returned has started its service: a panic is a +// failed start, as it is for a constructor, or a caller that recovered it +// would be served a half-initialised service and Stop would pair an OnStop +// with an OnStart that never finished. The failure is recorded on the +// instance as well as returned, so a resolution that waited reports it too. +func (in *instance) startClaimed(ctx context.Context, owner *state) error { + err := in.start(ctx, owner) + owner.mu.Lock() + if err == nil { + in.ph = phaseStarted + } else { + in.ph = phaseFailed + if in.err == nil { + in.err = fmt.Errorf("di: starting %s (provided at %s): %w", in.b.key, in.b.where(), err) + } + } + wake(in.startingCh) + owner.mu.Unlock() + return err +} + +// paired reports whether the instance's OnStop has an OnStart to pair with: +// the binding declares one and the scope has been started. It walks the +// parent chain, so it is answered before the owning state's mutex is taken. +func (in *instance) paired(owner *state) bool { + return in.b.onStart != nil && owner.everStarted() +} + +// owes reports whether the instance owes its drain and stop steps: it +// started, or it was built and has no start step to pair with, so OnStop is +// a plain destructor. An instance whose OnStart failed or was skipped by a +// rollback owes nothing. Called with the owning state's mutex held. +func (in *instance) owes(paired bool) bool { + return in.ph == phaseStarted || (in.ph == phaseBuilt && !paired) +} + +// stopIfNeeded runs the stop step once, if it is owed. It first waits out +// whichever step another goroutine is still running for this instance -- a +// start step, then a drain hook -- so the release never runs against a value +// one of them holds. That wait is what makes Stop synchronous, and it is safe +// because a hook may not call Stop on its own scope or an ancestor. +// +// If ctx expires first the release is still owed, and this is the only caller +// that can make it happen: Stop took the instance off its scope's list before +// the walk began. So the deadline ends the caller's wait, not the teardown, +// which finishes on a goroutine of its own once the step returns, with the +// spent deadline dropped so that it waits properly. +func (in *instance) stopIfNeeded(ctx context.Context, owner *state) error { + paired := in.paired(owner) + for { + owner.mu.Lock() + step, what := in.outstanding() + if step == nil { + owed := in.owes(paired) + in.ph = phaseStopped + owner.mu.Unlock() + if !owed { + return nil + } + return in.stop(ctx, owner) + } + owner.mu.Unlock() + select { + case <-step: + case <-ctx.Done(): + go func() { _ = in.stopIfNeeded(context.WithoutCancel(ctx), owner) }() + return fmt.Errorf("di: stopping %s: %s did not return: %w", in.b.key, what, ctx.Err()) + } + } +} + +// outstanding names the step another goroutine is running for this instance, +// with the channel that goroutine will close, or nil if the instance is +// nobody else's business. Called with the owning state's mutex held, so the +// phase and the channel are read in one critical section. +func (in *instance) outstanding() (chan struct{}, string) { + switch { + case in.ph == phaseStarting: + return waitOn(&in.startingCh), "OnStart" + case in.dr == draining: + return waitOn(&in.drainedCh), "OnDrain" + } + return nil, "" +} + +// drainIfNeeded runs OnDrain once, if it is owed: a service that will not be +// stopped has nothing to wind down. It reports whether this call ran or waited +// for the hook, so a drain pass can tell that it did work. +// +// A drain another Stop has begun is waited for, not skipped, or this Stop +// would go on to run OnStop while that hook still holds the value. A start +// step in flight is waited for as well: a service that is starting owes a +// drain as soon as it has started. +func (in *instance) drainIfNeeded(ctx context.Context, owner *state) (bool, error) { + b := in.b + if b.onDrain == nil { + return false, nil + } + paired := in.paired(owner) + for { + owner.mu.Lock() + if in.dr == drained { + owner.mu.Unlock() + return false, nil + } + if in.dr == draining { + done := waitOn(&in.drainedCh) + owner.mu.Unlock() + select { + case <-done: + return true, nil + case <-ctx.Done(): + return true, fmt.Errorf("di: draining %s: another Stop did not finish OnDrain: %w", b.key, ctx.Err()) + } + } + if in.ph == phaseStarting { + starting := waitOn(&in.startingCh) + owner.mu.Unlock() + select { + case <-starting: + continue + case <-ctx.Done(): + return false, fmt.Errorf("di: draining %s: OnStart did not return: %w", b.key, ctx.Err()) + } + } + if owner.isStopped() || !in.owes(paired) { + // Not owed, or the scope's own Stop has moved past draining and a + // sweep still running in an ancestor reached an instance built + // into it: winding it down for work it can no longer take on is + // the opposite of what the hook is for. Straight to drained, so + // no waiter can arrive and no channel is needed. + in.dr = drained + owner.mu.Unlock() + return false, nil + } + in.dr = draining + owner.mu.Unlock() + break + } + + t0 := time.Now() + err := callHook(b.onDrain, inHook(ctx, owner), in.value) + owner.report(EventDrain, b, t0, err) + + owner.mu.Lock() + in.dr = drained + wake(in.drainedCh) + owner.mu.Unlock() + + if err != nil { + return true, fmt.Errorf("di: draining %s: %w", b.key, err) + } + return true, nil +} + +// stop cancels the Worker hook, waits for it within ctx, then runs OnStop. +// +// A Worker hook that outlasts ctx still holds the value, so OnStop cannot run yet +// without racing the worker. The missed deadline is reported to the caller and +// the release is finished when the worker returns, as Stop does for a start +// step in flight. +func (in *instance) stop(ctx context.Context, owner *state) error { + b := in.b + if in.cancel == nil && b.onStop == nil { + return nil + } + t0 := time.Now() + var errs []error + if in.cancel != nil { + in.cancel() + select { + case <-in.runDone: + if in.runErr != nil { + errs = append(errs, in.runErr) + } + case <-ctx.Done(): + err := fmt.Errorf("di: stopping %s: Worker hook did not return: %w", b.key, ctx.Err()) + if b.onStop == nil { + owner.report(EventStop, b, t0, err) + return err + } + go in.releaseAfterWorker(context.WithoutCancel(ctx), owner, err) + return err + } + } + if b.onStop != nil { + if err := callHook(b.onStop, inHook(ctx, owner), in.value); err != nil { + errs = append(errs, fmt.Errorf("di: stopping %s: %w", b.key, err)) + } + } + err := errors.Join(errs...) + owner.report(EventStop, b, t0, err) + return err +} + +// releaseAfterWorker finishes a stop step whose Worker hook outlasted Stop's +// context, once the hook returns. missed is what Stop returned to its caller; +// the instance's single EventStop is emitted here and carries it along with +// the release's own result, so no observer sees a service stopped twice. +func (in *instance) releaseAfterWorker(ctx context.Context, owner *state, missed error) { + <-in.runDone + b := in.b + t0 := time.Now() + errs := []error{missed} + if in.runErr != nil { + errs = append(errs, in.runErr) + } + if err := callHook(b.onStop, inHook(ctx, owner), in.value); err != nil { + errs = append(errs, fmt.Errorf("di: stopping %s: %w", b.key, err)) + } + owner.report(EventStop, b, t0, errors.Join(errs...)) +} + +// onlyCancellation reports whether err says nothing beyond context.Canceled: +// the cancellation itself, or wrappings of it. A worker that returns +// errors.Join(ctx.Err(), failure) after being cancelled is reporting the +// failure, and errors.Is would have called the whole thing a cancellation. +func onlyCancellation(err error) bool { + if err == context.Canceled { + return true + } + switch u := err.(type) { + case interface{ Unwrap() []error }: + errs := u.Unwrap() + if len(errs) == 0 { + return false + } + for _, e := range errs { + if !onlyCancellation(e) { + return false + } + } + return true + case interface{ Unwrap() error }: + inner := u.Unwrap() + return inner != nil && onlyCancellation(inner) + } + return false +} + +// Start builds every Eager binding in registration order, then runs the +// start step of everything built so far, in build order. If a constructor +// or a start step fails, the scope is stopped, which rolls back exactly the +// services that did start, child scopes included. A service that was built +// but never started is not stopped, so acquire resources in OnStart rather +// than in the constructor when the binding declares one. +// +// After Start returns, a service built later runs its start step as part of +// being built, so lazily resolved services start too. Start may be called +// once, and builds only this scope's own Eager bindings: a child scope's are +// built by that child's Start. +func (s *Scope) Start(ctx context.Context) error { + // Start has no deadline of its own for the rollback, so it detaches the + // caller's context: an already-cancelled ctx must not skip the teardown. + return s.start(ctx, func() (context.Context, func()) { + return context.WithoutCancel(ctx), func() {} + }) +} + +// start is Start with the rollback context supplied by the caller: Start +// detaches the caller's context, Run applies its StopTimeout and signal +// handling. +func (s *Scope) start(ctx context.Context, rollbackCtx func() (context.Context, func())) (err error) { + defer recoverAbort(&err) + s.freeze() + s.mu.Lock() + if s.startCtx != nil { + s.mu.Unlock() + return errors.New("di: Start called twice") + } + s.startCtx = ctx + eager := slices.Clone(s.eager) // derived at freeze; clone so a later freeze cannot truncate it + s.mu.Unlock() + + // A failing eager constructor must roll back like a failing hook. + if err := s.buildEager(eager); err != nil { + return errors.Join(err, s.rollback(rollbackCtx)) + } + + s.mu.Lock() + s.running = true + s.mu.Unlock() + + // Drain: anything built before the flag was set is still waiting here, + // and starting one service may build more. + for { + in, owner := s.claimNext() + if in == nil { + if s.isStopped() { + // A start hook stopped the scope; nothing is running. + return fmt.Errorf("di: Start: %w", ErrStopped) + } + return nil + } + if err := in.startClaimed(ctx, owner); err != nil { + err = fmt.Errorf("di: starting %s: %w", in.b.key, err) + return errors.Join(err, s.rollback(rollbackCtx)) + } + } +} + +func (s *Scope) rollback(mk func() (context.Context, func())) error { + ctx, cancel := mk() + defer cancel() + return s.Stop(ctx) +} + +// buildEager builds the eager bindings, turning a constructor failure into +// an error rather than letting it unwind past Start's rollback. +func (s *Scope) buildEager(eager []*binding) (err error) { + defer recoverAbort(&err) + for _, b := range eager { + if b.group { + // A group member is not reachable by key: resolve it directly. + s.enter().resolve(b, s.state) + continue + } + // By key, so whichever registration owns the key is what gets built; + // deriveEager has already checked that it can honour eagerness. + s.enter().get(b.key) + } + return nil +} + +// claimNext claims the start step of the next built-but-unstarted instance +// in this scope or a descendant, in build order. +func (st *state) claimNext() (*instance, *state) { + st.mu.Lock() + for _, in := range st.started { + if in.ph == phaseBuilt { + in.ph = phaseStarting + st.mu.Unlock() + return in, st + } + } + children := slices.Clone(st.children) + st.mu.Unlock() + for _, c := range children { + if in, owner := c.claimNext(); in != nil { + return in, owner + } + } + return nil, nil +} + +// Context returns the context passed to Start (or Run) on this scope or the +// nearest started ancestor, so constructors can dial with a deadline. Before +// Start it returns context.Background(). +func (s *Scope) Context() context.Context { + if ctx, _ := s.runContext(); ctx != nil { + return ctx + } + return context.Background() +} + +// Stop winds the scope down in three phases. First it drains: OnDrain hooks +// run from the innermost scope outwards, in reverse build order, while every +// scope still resolves, so work already in flight can finish and still reach +// its dependencies. A service or child scope that phase brings into being is +// drained too, before anything is marked stopped. Then the scope is marked +// stopped and child scopes are stopped. Then OnStop hooks run in reverse +// build order (dependents first). +// +// A service is stopped only if it started, or if it declares no OnStart, in +// which case OnStop is a plain destructor. Every failure is reported. +// +// Stop is synchronous. It waits out whatever another goroutine is still +// running for a service it is tearing down -- a start step in flight, a drain +// hook another Stop began, a Worker hook being cancelled -- so when it returns, +// the teardown has happened and its failures are in the error. A teardown +// outlives the call only when ctx expires first: the missed deadline is +// reported here, and the release is finished once the outstanding step +// returns, on a goroutine of its own, reaching observers rather than this +// caller. +// +// Afterwards the scope and its descendants refuse to resolve anything, with +// ErrStopped; that includes a resolution that was already waiting when the +// scope stopped, so a closed service is never handed out. Stopping a child +// scope also detaches it from its parent, so per-request scopes are released +// once stopped. +// +// Stop is idempotent, and concurrent calls are safe: only the first tears the +// scope down, and the others wait for it and report its result, bounded by +// their own context. Two Stop calls that meet at one scope, as a child and +// its parent do, wait for each other phase by phase, so neither starts +// releasing what the other's hooks are still using. +// +// Because Stop waits, a hook must not call Stop on its own scope or an +// ancestor: it would be waiting for the step it is itself running. Stopping a +// sibling, or a scope below the hook's own, is allowed. A hook that passes on +// the context it was given gets an error saying so; one that passes a context +// of its own is not recognised, and waits until that context expires. Call +// Shutdown, which never blocks. +func (s *Scope) Stop(ctx context.Context) error { + if h := hookOwner(ctx); h != nil && h.descendsFrom(s.state) { + return fmt.Errorf("di: a lifecycle hook of scope %s called Stop on scope %s, which it is inside: call Shutdown instead", h.name, s.name) + } + if !s.stopOnce.claim(s.state, func() { + if s.stopCtx == nil { + s.stopCtx = ctx // the first Stop owns it; a later call must not clobber it + } + }) { + finished, err := s.stopOnce.wait(s.state, ctx) + if !finished { + return fmt.Errorf("di: waiting for scope %s to stop: %w", s.name, ctx.Err()) + } + return err + } + err := s.teardown(ctx) + s.stopOnce.settle(s.state, err) + return err +} + +// teardown is the body of the first Stop. +func (s *Scope) teardown(ctx context.Context) error { + errs := []error{s.drain(ctx)} + + s.mu.Lock() + children := slices.Clone(s.children) + started := s.started + s.started = nil + s.stopped.Store(true) + s.mu.Unlock() + + for _, c := range children { + errs = append(errs, (&Scope{state: c}).Stop(ctx)) + } + errs = append(errs, stopAll(ctx, s.state, started)) + + if p := s.parent; p != nil { + p.mu.Lock() + p.children = slices.DeleteFunc(p.children, func(c *state) bool { return c == s.state }) + p.mu.Unlock() + } + return errors.Join(errs...) +} + +// drain runs the OnDrain hooks of this scope's subtree before anything is +// marked stopped, innermost first and in reverse build order, the order Stop +// uses. Nothing here changes an instance's phase. +// +// Only the first drain of a scope runs; a Stop that reaches the scope by +// another route waits for it. Without that wait, when a child and its parent +// are stopped at once, the second Stop would walk past a drain still in +// flight and start releasing what its hooks are using. +func (s *Scope) drain(ctx context.Context) error { + if !s.drainOnce.claim(s.state, nil) { + // The owner settles the phase with what this scope's own hooks + // reported, and a Stop reports that whether it ran the hooks or + // waited for someone else to. + finished, err := s.drainOnce.wait(s.state, ctx) + if !finished { + return fmt.Errorf("di: waiting for scope %s to drain: %w", s.name, ctx.Err()) + } + return err + } + root := &drainScope{st: s.state, ours: true} + r := drainRun{root: root, seen: map[*state]*drainScope{s.state: root}} + err := r.sweepAll(ctx) + s.drainOnce.settle(s.state, err) // this scope's phase is the last to end + return err +} + +// drainRun is the bookkeeping of one drain phase: the scopes it has reached, +// whether it owns each one's phase, and whether that phase has ended. +type drainRun struct { + root *drainScope + seen map[*state]*drainScope +} + +type drainScope struct { + st *state + ours bool // this run claimed the phase; otherwise another Stop owns it + settled bool // its phase has ended; for a descendant, when its own sweep does +} + +// sweepAll is the body of the first drain. It sweeps the subtree until a pass +// finds no new work, because the scope still resolves during this phase: a +// hook finishing in-flight work may build a service or open a child scope, +// and those owe a drain too, before anything is marked stopped. ctx bounds +// the sweep as well as the hooks, so a hook that keeps building cannot hold +// the phase open for ever. +func (r *drainRun) sweepAll(ctx context.Context) error { + var errs []error + for { + progress := false + errs = append(errs, r.visit(ctx, r.root, &progress)...) + if !progress || ctx.Err() != nil { + return errors.Join(errs...) + } + } +} + +// visit sweeps one scope this run owns and everything below it, innermost +// first and in reverse creation order, the order Stop uses. Every owned scope +// is swept on every pass, not only the ones that appeared in it, because a +// hook may build into a scope already visited. +// +// It returns the errors that belong to *this* scope's Stop. A descendant's are +// settled into that descendant's phase instead, so its own Stop reports them, +// and they reach this caller through teardown, which stops its children and +// joins what their Stop returns; that is what keeps one failure to one place +// in the aggregate. Errors found in a descendant after its phase has ended +// have nowhere to be settled, so those bubble up here. +// +// A descendant's phase is claimed just before its subtree is swept and ended +// as soon as that sweep finishes, so while a hook runs the only unended phases +// this run holds are the scope being swept and its ancestors, which a hook may +// not Stop anyway. Claiming the whole subtree up front would deadlock a hook +// that stops a scope the walk has claimed but not yet reached, such as a +// server draining in one child and stopping a request scope in another. +// +// A scope another Stop already owns is waited for and then left alone, +// subtree included; that Stop's run drains it. +func (r *drainRun) visit(ctx context.Context, ds *drainScope, progress *bool) []error { + var errs []error + ds.st.mu.Lock() + children := slices.Clone(ds.st.children) + ds.st.mu.Unlock() + for _, c := range slices.Backward(children) { + cs := r.seen[c] + if cs == nil { + *progress = true + cs = &drainScope{st: c} + r.seen[c] = cs + if c.drainOnce.claim(c, nil) { + cs.ours = true + } else if finished, _ := c.drainOnce.wait(c, ctx); !finished { + errs = append(errs, fmt.Errorf("di: waiting for scope %s to drain: %w", c.name, ctx.Err())) + } + } + if cs.ours { + errs = append(errs, r.visit(ctx, cs, progress)...) + } + } + ds.st.mu.Lock() + started := slices.Clone(ds.st.started) + ds.st.mu.Unlock() + for _, in := range slices.Backward(started) { + ran, err := in.drainIfNeeded(ctx, ds.st) + *progress = *progress || ran + errs = append(errs, err) + } + if ds != r.root && !ds.settled { + ds.st.drainOnce.settle(ds.st, errors.Join(errs...)) + ds.settled = true + return nil // reported by this scope's own Stop, not by its parent's + } + return errs +} + +func stopAll(ctx context.Context, owner *state, started []*instance) error { + var errs []error + for _, in := range slices.Backward(started) { + errs = append(errs, in.stopIfNeeded(ctx, owner)) + } + return errors.Join(errs...) +} diff --git a/resolve.go b/resolve.go new file mode 100644 index 0000000..5d9c139 --- /dev/null +++ b/resolve.go @@ -0,0 +1,534 @@ +package di + +// Resolution: the path a resolution walks, the two cycle detectors, the +// build step of an instance, and the entry points that resolve by type. + +import ( + "errors" + "fmt" + "reflect" + "slices" + "sync" + "sync/atomic" + "time" +) + +// abort is the panic a wiring failure unwinds with. It reaches the nearest +// Resolve, Start or Run and becomes that call's error; a top-level Get panics +// with the plain error instead. +type abort struct{ err error } + +// resolver is one node of a resolution path: what is being resolved and the +// node that needed it. The path is a linked list, not a slice, because a +// constructor may resolve from several goroutines at once; a node is never +// mutated after it is made (except done), so branches share nothing and each +// carries the whole path for cycle detection and error messages. +// +// A node is identified by binding and holder, not by key: a group member and +// a plain registration of the same type are different bindings, and one +// Scoped binding is a different node in each scope that holds an instance of +// it. +type resolver struct { + parent *resolver + b *binding // nil on the root node, which resolves nothing itself + holder *state + + // done marks a node whose resolution has returned. The path stays whole + // for error messages, but a finished node is no longer a dependency: a + // constructor may keep the Scope it was handed and resolve through it + // later, and that resolution must not meet its own finished frame and be + // called a cycle. Written once by the resolution that owns the node, read + // from any branch. + done atomic.Bool +} + +func (r *resolver) child(b *binding, holder *state) *resolver { + return &resolver{parent: r, b: b, holder: holder} +} + +// onPath reports whether this exact binding is still being resolved further +// up the path, which is a dependency cycle within one branch. +// +// The walk stops at the first finished node rather than skipping it. Only a +// constructor that kept its Scope can put one on a live path, and a +// resolution made through that Scope afterwards is a new branch: what is +// above the finished node may still be building, but not for it, so it has +// only to wait. The one shape this cannot tell apart without goroutine-local +// state deadlocks instead of being reported: a constructor that blocks on a +// resolution made through a finished descendant's Scope that leads back to +// itself. +func (r *resolver) onPath(b *binding, holder *state) bool { + for n := r; n != nil; n = n.parent { + if n.done.Load() { + return false + } + if n.b == b && n.holder == holder { + return true + } + } + return false +} + +func (r *resolver) path() string { + if r == nil || r.b == nil { + return "" + } + return " (needed by " + r.pathStr() + ")" +} + +func (r *resolver) pathStr() string { + var parts []string + for n := r; n != nil; n = n.parent { + if n.b != nil { + parts = append(parts, n.b.key.String()) + } + } + slices.Reverse(parts) + return fmt.Sprint(parts) +} + +// graph is one container's wait-for graph, with two kinds of edge: an +// instance points at the resolution building it (instance.builder), and a +// blocked resolution points at the instance it waits for (blockedFor). Its +// mutex is the innermost lock: a state's mutex may be held while taking it, +// never the reverse, so the graph can be read across scopes without ordering +// state mutexes against each other. +// +// New makes one graph per container and every scope under that root shares +// it. That is as far as a cycle can reach: a resolution follows the parent +// chain, so a wait can cross scopes, but nothing joins two containers. +type graph struct { + mu sync.Mutex + blockedFor map[*resolver]*instance +} + +// descends reports whether n is anc or was created below it. A branch blocks +// at a leaf of its path, several nodes below the one that claimed the build it +// is holding up, so both directions of the graph are matched against whole +// paths rather than single nodes. +// +// The walk stops at a node whose resolution has returned, for the same reason +// onPath does: nothing above a finished node is waiting for what is opened +// below it later. +func descends(n, anc *resolver) bool { + for ; n != nil; n = n.parent { + if n == anc { + return true + } + if n.done.Load() { + return false + } + } + return false +} + +// wait records that r is about to wait for in, unless that would close a +// wait-for cycle by reaching, through builds that are themselves blocked, a +// build this branch is responsible for finishing. Called with the holder's +// mutex held; the check and the new edge are one critical section, so two +// branches closing a cycle at once cannot both decide to wait. +func (r *resolver) wait(g *graph, in *instance) bool { + g.mu.Lock() + defer g.mu.Unlock() + + seen := map[*instance]bool{in: true} + for stack := []*instance{in}; len(stack) > 0; { + cur := stack[len(stack)-1] + stack = stack[:len(stack)-1] + builder := cur.builder + if builder == nil { + continue // nobody is building it: whoever holds it will settle it + } + if descends(r, builder) { + return false // waiting on our own branch's work + } + for n, j := range g.blockedFor { + if descends(n, builder) && !seen[j] { + seen[j] = true + stack = append(stack, j) + } + } + } + g.blockedFor[r] = in + return true +} + +func (r *resolver) unwait(g *graph) { + g.mu.Lock() + delete(g.blockedFor, r) + g.mu.Unlock() +} + +// as unwraps a stored value. A nil interface is a legitimate service, and a +// nil any cannot be asserted back to the interface type it was stored as, so +// it becomes T's zero value rather than a panic. Every hand-back of a stored +// value goes through here. +func as[T any](v any) T { + if v == nil { + var zero T + return zero + } + return v.(T) +} + +// lookup finds the binding registered for k in this scope or an ancestor, +// and the scope that owns it. A nil binding means nothing is registered for +// k anywhere in the chain. +func (s *Scope) lookup(k key) (*binding, *state) { + for st := s.state; st != nil; st = st.parent { + st.freeze() + st.mu.Lock() + b, ok := st.index[k] + st.mu.Unlock() + if ok { + return b, st + } + } + return nil, nil +} + +// inFlight reports whether this Scope is a live view of a resolution: it +// carries a path whose last node has not returned. A Scope kept past that +// point, by a constructor or by a Child made in one, is not in flight, and +// calls through it are top-level calls. +func (s *Scope) inFlight() bool { return s.r != nil && !s.r.done.Load() } + +// enter returns a view carrying a resolver, starting a new resolution unless +// one is in flight. +func (s *Scope) enter() *Scope { + if s.inFlight() { + return s + } + return s.view(&resolver{}) +} + +// get resolves k. Outside a constructor the internal abort is converted into +// a panic carrying the plain error; inside one it unwinds to the enclosing +// Resolve/Start call. +func (s *Scope) get(k key) any { + if !s.inFlight() { + defer unwrapAbort() + return s.enter().get(k) + } + b, owner := s.lookup(k) + if b == nil { + panic(abort{fmt.Errorf("di: %s: %w%s", k, ErrNotProvided, s.r.path())}) + } + v := s.resolve(b, owner) + s.markServed(owner, k) + return v +} + +// markServed records that k was served to this scope from owner, in every +// scope between the two. binding.used protects the owner; the scopes in +// between each handed out a value for k as well, and registering k in one of +// them afterwards would give the key two live values there. +func (s *Scope) markServed(owner *state, k key) { + for st := s.state; st != nil && st != owner; st = st.parent { + st.mu.Lock() + if st.served == nil { + st.served = make(map[key]bool, 4) + } + st.served[k] = true + st.mu.Unlock() + } +} + +// resolve produces b's value for the resolving scope s, honouring the +// binding's lifetime and starting the instance when the scope is running. +func (s *Scope) resolve(b *binding, owner *state) any { + if s.isStopped() { + panic(abort{fmt.Errorf("di: %s: %w%s", b.key, ErrStopped, s.r.path())}) + } + // The holder owns the instance's lifecycle: a singleton lives in the + // scope that registered the binding, a scoped one in the scope that + // resolves it, so it can see that scope's values. + holder := owner + if b.scoped { + holder = s.state + } + if s.r.onPath(b, holder) { + panic(abort{fmt.Errorf("di: %w: %s -> %s", ErrCycle, s.r.pathStr(), b.key)}) + } + if !b.used.Load() { + // Hold the key against an override for as long as this resolution + // runs, so a constructor cannot replace the registration it is + // itself being built from. used is set before this is dropped, so + // the two guards never leave a gap between them. + b.resolving.Add(1) + defer b.resolving.Add(-1) + } + sc := s.view(s.r.child(b, holder)) + // The node stops being a dependency when this resolution returns, however + // it returns. See resolver.done. + defer sc.r.done.Store(true) + + in := holder.instanceFor(b) + v, err := sc.await(in, holder) + if err != nil { + panic(abort{err}) + } + b.used.Store(true) + // The edge belongs to the node that asked, and only a node with a binding + // has an instance to record it on: a top-level Get starts a path whose + // first node has none, and so does a Scope kept past its resolution. The + // test is here rather than in dependOn so that the warm path pays a + // pointer comparison instead of a call. A failed resolution records + // nothing; the path it failed on is in the error. + if s.r.b != nil { + s.r.dependOn(in, holder) + } + return v +} + +// dependOn records that the resolution at this node needed in, once per +// distinct dependency: a constructor that asks for the same service twice +// gets one edge. The scan is over one constructor's own dependencies and runs +// only while it is building. +func (r *resolver) dependOn(in *instance, holder *state) { + r.holder.mu.Lock() + defer r.holder.mu.Unlock() + // resolve made the asking instance before running its constructor, so + // the nil check is a guard on the recording only: a mistake here must + // not break resolution. + asker := r.holder.instanceAt(r.b) + if asker == nil || slices.ContainsFunc(asker.deps, func(d dep) bool { return d.in == in }) { + return + } + asker.deps = append(asker.deps, dep{in: in, holder: holder}) +} + +// await returns the instance's value: this branch builds it if it gets there +// first, and otherwise waits for whoever did. It waits for the start step as +// well, so a resolution of a running scope never hands out a service whose +// OnStart is still in flight. A wait that would close a cycle between two +// concurrent builds is reported as ErrCycle rather than deadlocking. +func (s *Scope) await(in *instance, holder *state) (any, error) { + holder.mu.Lock() + for in.ph == phaseNew || !in.settled || in.ph == phaseStarting { + if in.ph == phaseNew { + in.claimBuild(holder, s.r) + holder.mu.Unlock() + s.materialise(in, holder) + holder.mu.Lock() + continue + } + // The phase says which step is outstanding and so which channel to + // block on. Both are read in this critical section, and the owner + // closes the channel under the same mutex, so it cannot be closed + // between the choice and the block. + var ready chan struct{} + if in.settled { + ready = waitOn(&in.startingCh) // settled, so OnStart is outstanding + } else { + ready = waitOn(&in.settledCh) + } + if !s.r.wait(holder.graph, in) { + holder.mu.Unlock() + return nil, fmt.Errorf("di: %w: %s -> %s", ErrCycle, s.r.parent.pathStr(), in.b.key) + } + holder.mu.Unlock() + <-ready + s.r.unwait(holder.graph) + holder.mu.Lock() + } + value, err := in.value, in.err + if err == nil && s.isStopped() { + // The scope stopped while this branch was building or waiting; + // resolve's check was before the wait. The check is on the resolving + // scope, which covers the holder (always that scope or an ancestor): + // a stopped scope must refuse the request whether or not the value is + // still alive above it. + value, err = nil, fmt.Errorf("di: %s: %w", in.b.key, ErrStopped) + } + holder.mu.Unlock() + return value, err +} + +// claimBuild takes the build step for this resolution. Called with the +// holder's mutex held. +func (in *instance) claimBuild(holder *state, r *resolver) { + in.ph = phaseBuilding + g := holder.graph + g.mu.Lock() + in.builder = r + g.mu.Unlock() +} + +// settle publishes the outcome of the build step and wakes every resolution +// waiting for this instance. +func (in *instance) settle(holder *state) { + holder.mu.Lock() + in.settled = true + g := holder.graph + g.mu.Lock() + in.builder = nil + g.mu.Unlock() + wake(in.settledCh) + holder.mu.Unlock() +} + +// fail records a build failure, which is terminal for the instance. +func (in *instance) fail(holder *state, err error) { + holder.mu.Lock() + in.ph, in.err = phaseFailed, err + holder.mu.Unlock() +} + +// materialise builds an instance, once. A failure is recorded on the instance +// rather than unwound, so every later resolution reports it identically, and +// the instance is settled on the way out so waiters are released whatever +// happened. +func (s *Scope) materialise(in *instance, holder *state) { + defer in.settle(holder) + if err := s.construct(in, holder); err != nil { + in.fail(holder, err) + return + } + if !in.publish(holder) { + return + } + in.startIfRunning(holder) +} + +// construct runs the constructor, turning a panic or an abort from a nested +// resolution into an error, and reports the attempt to observers either way. +func (s *Scope) construct(in *instance, holder *state) (err error) { + b := in.b + t0 := time.Now() + defer func() { + if rec := recover(); rec != nil { + if a, ok := rec.(abort); ok { + err = fmt.Errorf("di: building %s (provided at %s): %w", b.key, b.where(), a.err) + } else { + err = fmt.Errorf("di: building %s (provided at %s): panic: %v", b.key, b.where(), rec) + } + } + holder.report(EventBuild, b, t0, err) + }() + in.value = b.build(&Scope{state: holder, r: s.r, module: b.module}) + return nil +} + +// publish adds the instance to its owner's stop list so it will be torn +// down. If Stop ran while the constructor was in flight its snapshot did not +// include this instance, so undo it here and report ErrStopped instead. +func (in *instance) publish(owner *state) bool { + owner.mu.Lock() + stopped := owner.isStopped() + in.ph = phaseBuilt + if !stopped { + owner.started = append(owner.started, in) + } + owner.mu.Unlock() + if !stopped { + return true + } + err := errors.Join(fmt.Errorf("di: %s: %w", in.b.key, ErrStopped), in.stopIfNeeded(owner.stopContext(), owner)) + owner.mu.Lock() + in.err = err + owner.mu.Unlock() + return false +} + +// startIfRunning runs the start step when the scope is already running. +// publish strictly precedes the read below, and Start sets running before it +// drains, so either this starts the instance or Start's drain finds it. +// startClaimed records its failure on the instance, so a resolution that +// waited for the step reports it too. +func (in *instance) startIfRunning(owner *state) { + if sctx, running := owner.runContext(); running && in.claim(owner) { + _ = in.startClaimed(sctx, owner) + } + stopped := owner.isStopped() + owner.mu.Lock() + if in.err == nil && stopped { + // Stop ran while we were starting and waited for the step, so the + // instance is torn down: do not hand it out. + in.err = fmt.Errorf("di: %s: %w", in.b.key, ErrStopped) + } + owner.mu.Unlock() +} + +// Get resolves T. Inside a constructor, failure unwinds to the enclosing +// Resolve/Start call and becomes an error; at top level it panics. In a +// goroutine a constructor started, use Resolve instead: that panic has no +// enclosing call to unwind to and would take the process down. +func (s *Scope) Get[T any]() T { return as[T](s.get(key{t: reflect.TypeFor[T]()})) } + +// Maybe resolves T if it is provided anywhere in the scope chain. +func (s *Scope) Maybe[T any]() (T, bool) { + if b, _ := s.lookup(key{t: reflect.TypeFor[T]()}); b == nil { + var zero T + return zero, false + } + return s.Get[T](), true +} + +// All resolves the multi-binding group for T across the scope chain. Members +// are singletons (or Scoped if so marked) with the same lifecycle +// as any other binding. +func (s *Scope) All[T any]() []T { + if !s.inFlight() { + defer unwrapAbort() + return s.enter().All[T]() + } + k := key{t: reflect.TypeFor[T]()} + var out []T + for st := s.state; st != nil; st = st.parent { + st.freeze() + st.mu.Lock() + bs := slices.Clone(st.groups[k]) + st.mu.Unlock() + for _, b := range bs { + out = append(out, as[T](s.resolve(b, st))) + } + } + return out +} + +// Must unwraps a (value, error) pair inside a constructor: +// +// db := s.Must(sql.Open("postgres", dsn)) +// +// A non-nil error aborts the constructor and surfaces from the enclosing +// Resolve, Start or Run. Outside a constructor it panics with the error. +func (s *Scope) Must[T any](v T, err error) T { + if err == nil { + return v + } + if s.inFlight() { + panic(abort{err}) + } + panic(err) +} + +// Resolve resolves T, reporting a wiring failure as an error rather than a +// panic. It is the entry point for a goroutine a constructor started. +func (s *Scope) Resolve[T any]() (v T, err error) { + defer recoverAbort(&err) + return as[T](s.enter().get(key{t: reflect.TypeFor[T]()})), nil +} + +// unwrapAbort turns an abort into a panic carrying the plain error, which is +// what a top-level Get or All reports. Deferred only by an entry point that +// is not already inside a resolution. +func unwrapAbort() { + if rec := recover(); rec != nil { + if a, ok := rec.(abort); ok { + panic(a.err) + } + panic(rec) + } +} + +// recoverAbort turns an abort into *err and re-panics anything else. +func recoverAbort(err *error) { + if rec := recover(); rec != nil { + if a, ok := rec.(abort); ok { + *err = a.err + return + } + panic(rec) + } +} diff --git a/run.go b/run.go new file mode 100644 index 0000000..de2a001 --- /dev/null +++ b/run.go @@ -0,0 +1,126 @@ +package di + +// Run and Shutdown: the main-function loop over Start and Stop, and the +// signal handling around it. + +import ( + "context" + "errors" + "os" + "os/signal" + "syscall" + "time" +) + +// Shutdown asks a running Run to stop and records the cause it should return. +// It never blocks, may be called from any goroutine, and the first call wins. +// It propagates to ancestor scopes, so a service in a child scope can stop the +// application. +func (s *Scope) Shutdown(cause error) { + first := false + for st := s.state; st != nil; st = st.parent { + st.shutdownOnce.Do(func() { + st.shutdownErr = cause + close(st.shutdownCh) + first = first || st == s.state + }) + } + if first { + s.emit(Event{Kind: EventShutdown, Scope: s.name, Err: cause}) + } +} + +// RunOption configures Run. +type RunOption func(*runConfig) + +type runConfig struct{ stopTimeout time.Duration } + +// exitSignals are what make Run exit: an interrupt or a termination request. +var exitSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} + +// StopTimeout bounds how long Stop may take once Run decides to exit. +// The default is 15 seconds. +func StopTimeout(d time.Duration) RunOption { return func(c *runConfig) { c.stopTimeout = d } } + +// stopContext builds the context Run stops with: detached from the caller's, +// bounded by StopTimeout, and cancelled by a second signal so a hung hook +// cannot keep the process alive. A rollback from a failed Start gets the same +// context. +func (c runConfig) stopContext(ctx context.Context) (context.Context, func()) { + stopCtx, cancelStop := context.WithTimeout(context.WithoutCancel(ctx), c.stopTimeout) + forceCtx, cancelForce := signal.NotifyContext(stopCtx, exitSignals...) + return forceCtx, func() { cancelForce(); cancelStop() } +} + +// Run starts the scope and blocks until ctx is cancelled, a termination +// signal arrives, or Shutdown is called. It then stops the scope with a +// bounded context; a second signal during the stop cancels that context so +// a hung hook cannot keep the process alive. Run returns the Start error, the +// error passed to Shutdown, and any Stop errors, joined. A worker that died +// on its own is reported once, whether it reached Run as the cause or as a +// Stop error. +func (s *Scope) Run(ctx context.Context, opts ...RunOption) error { + cfg := runConfig{stopTimeout: 15 * time.Second} + for _, o := range opts { + o(&cfg) + } + + // Register before Start so a signal during a slow start is not lost. + sigCtx, cancelSig := signal.NotifyContext(ctx, exitSignals...) + defer cancelSig() + + if err := s.start(ctx, func() (context.Context, func()) { return cfg.stopContext(ctx) }); err != nil { + // A failed Start rolls back through Stop, which runs the drain and + // stop hooks, so a worker can die and publish its failure here + // exactly as it can during an ordinary shutdown. + return joinCause(err, s.publishedCause()) + } + + var cause error + select { + case <-sigCtx.Done(): + case <-s.shutdownCh: + cause = s.shutdownErr + } + + stopCtx, cancel := cfg.stopContext(ctx) + defer cancel() + + stopErr := s.Stop(stopCtx) + if cause == nil { + // A worker that died during the stop published its failure through + // Shutdown after the select above had woken for a signal or a + // cancelled ctx. Read it again: the Stop that saw the failure may have + // been a child's, called from a drain hook that handled the error + // itself. + cause = s.publishedCause() + } + return joinCause(stopErr, cause) +} + +// publishedCause reports the failure Shutdown recorded, without waiting for +// one. Run reads it on both ways out, because a worker dies when it dies: +// during the stop that follows a signal, or during the rollback of a Start +// that never finished. +func (s *Scope) publishedCause() error { + select { + case <-s.shutdownCh: + return s.shutdownErr + default: + return nil + } +} + +// joinCause adds a published cause to what Run is already returning, unless +// that failure is in there already: a worker's error reaches Run by two +// routes, as the cause and through the Stop that cancelled it, and it is one +// failure either way. +func joinCause(err, cause error) error { + if cause == nil { + return err + } + if errors.Is(err, cause) { + return err // one failure, reached by both routes + } + return errors.Join(err, cause) +} diff --git a/state.go b/state.go new file mode 100644 index 0000000..0fc387a --- /dev/null +++ b/state.go @@ -0,0 +1,241 @@ +package di + +// A scope's state: its registry, the freeze that commits registrations into +// it, and the readers that walk the parent chain. No two state mutexes are +// ever ordered against each other; a walk takes and releases each in turn. + +import ( + "context" + "fmt" + "maps" + "slices" + "sync" + "sync/atomic" +) + +// state is a scope's registry and lifecycle bookkeeping. A Scope is a handle +// over it. +type state struct { + name string + parent *state + graph *graph // the container's wait-for graph, shared with every other scope under the root + + mu sync.Mutex + pending []*binding // registrations not yet indexed + index map[key]*binding + groups map[key][]*binding + frozen bool + all []*binding // every binding, in registration order + eager []*binding // derived by deriveEager: what Start builds + started []*instance // build order; stopped in reverse + scoped map[*binding]*instance // per-scope instances of Scoped bindings + served map[key]bool // keys this scope resolved from an outer scope; lazily made + children []*state + observers []func(Event) + + stopped atomic.Bool // set by Stop or a failed Start; resolution then fails with ErrStopped + stopCtx context.Context // the context Stop was called with + stopOnce once // this scope's teardown; later Stop calls wait for it + startCtx context.Context // set by Start; read by Context() + running bool // set once Start reaches the hook phase; enables late OnStart + + // drainOnce is the scope-wide drain phase, once-with-wait like stopOnce: + // a second Stop reaching this scope waits for the first drain instead of + // running its own or skipping past it. + drainOnce once + + shutdownOnce sync.Once + shutdownCh chan struct{} + shutdownErr error +} + +// freeze commits the pending registrations. The batch is validated against a +// copy of the registry and committed only if it passes, so a rejected +// registration leaves the scope as it was and is rejected identically on +// every later attempt. +func (st *state) freeze() { + st.mu.Lock() + defer st.mu.Unlock() + if len(st.pending) == 0 { + return + } + + index := maps.Clone(st.index) + groups := maps.Clone(st.groups) + all := slices.Clone(st.all) + for _, b := range st.pending { + // A wrapper is built where what it wraps is built, so it takes that + // lifetime, read here rather than at registration because the + // wrapped binding's own Scoped() may come later in the batch. + if b.inner != nil && b.inner.scoped { + b.scoped = true + } + b.validate() + if b.group { + groups[b.key] = append(slices.Clone(groups[b.key]), b) + } else { + prev, ok := index[b.key] + act := "overridden" + if b.inner != nil { + act = "wrapped" + } + switch { + case ok && !b.override && b.inner == nil: + // A replacement is a thing a caller declares; a later + // registration winning silently would let one module reroute + // another's wiring. + panic(fmt.Sprintf("di: %s is provided at %s and again at %s: a second registration of a key must be marked Override() to replace the first", + b.key, prev.where(), b.where())) + case !ok && b.override: + // Nearly always a fake for a service that was renamed, which + // would otherwise be a registration nobody resolves. A child + // shadows its parent without Override: that is a different + // registry, not a replacement. + panic(fmt.Sprintf("di: %s (provided at %s) is marked Override() but nothing in scope %s provides it; a child scope shadows its parent without Override", + b.key, b.where(), st.name)) + case ok && prev.used.Load(): + // Replacing or wrapping a key that has served a value would + // leave two live instances of one service. + panic(fmt.Sprintf("di: %s (provided at %s) cannot be %s at %s: it has already been resolved", + b.key, prev.where(), act, b.where())) + case ok && prev.resolving.Load() > 0: + // The same defect from the other side: the resolution in + // flight would return the old value while the replacement + // served everything it goes on to build. + panic(fmt.Sprintf("di: %s (provided at %s) cannot be %s at %s: it is being resolved", + b.key, prev.where(), act, b.where())) + case ok && b.inner == nil && prev.wrappedBy.Load() != nil: + // A wrapper, here or in a descendant, composes over prev; + // replacing prev would leave it serving a value built from a + // registration nothing else can reach. + panic(fmt.Sprintf("di: %s (provided at %s) cannot be overridden at %s: it is wrapped at %s", + b.key, prev.where(), b.where(), prev.wrappedBy.Load().where())) + } + if st.served[b.key] { + // This scope already handed the key down from an outer scope; + // shadowing it now would give the key two live values here. + panic(fmt.Sprintf("di: %s cannot be registered at %s: this scope has already resolved it from an outer scope", + b.key, b.where())) + } + index[b.key] = b + } + all = append(all, b) + } + eager := deriveEager(all, index) + + st.index, st.groups, st.all, st.eager = index, groups, all, eager + st.pending, st.frozen = nil, true +} + +// deriveEager returns the ordered set of bindings Start builds, and is the +// one place that decides what Eager means. For every key with an Eager +// registration, the set holds the binding that serves that key, once, at the +// position of the first such registration. A group member is its own entry. A +// binding with a per-scope lifetime cannot honour eagerness and is rejected +// here, whether declared so directly or arriving through an override. +func deriveEager(all []*binding, index map[key]*binding) []*binding { + var eager []*binding + seen := make(map[*binding]bool, len(all)) + for _, b := range all { + if !b.eager { + continue + } + w := b + if !b.group { + w = index[b.key] // whichever registration owns the key by now + } + if seen[w] { + continue + } + if w.scoped { + // b itself is caught by validate, so w is an override here. + panic(fmt.Sprintf("di: %s is Eager (provided at %s), but the Scoped registration at %s owns the key: eagerness cannot transfer to a per-scope lifetime", + b.key, b.where(), w.where())) + } + seen[w] = true + eager = append(eager, w) + } + return eager +} + +// descendsFrom reports whether st is anc or a scope under it. +func (st *state) descendsFrom(anc *state) bool { + for ; st != nil; st = st.parent { + if st == anc { + return true + } + } + return false +} + +// isStopped reports whether this scope or an ancestor has stopped. +func (st *state) isStopped() bool { + for ; st != nil; st = st.parent { + if st.stopped.Load() { + return true + } + } + return false +} + +// runContext walks up the scope chain to the nearest state that Start was +// called on. running reports whether that Start has passed its hook phase, +// which is when bindings built later must start themselves; it is never true +// with a nil ctx, since start records the context before setting the flag. +func (st *state) runContext() (ctx context.Context, running bool) { + for ; st != nil; st = st.parent { + st.mu.Lock() + ctx, running = st.startCtx, st.running + st.mu.Unlock() + if ctx != nil { + return ctx, running + } + } + return nil, false +} + +// everStarted reports whether Start was called on this scope or an ancestor. +func (st *state) everStarted() bool { + ctx, _ := st.runContext() + return ctx != nil +} + +// stopContext returns the context Stop was called with, or a background one +// if the scope was stopped without recording it. +func (st *state) stopContext() context.Context { + for ; st != nil; st = st.parent { + st.mu.Lock() + ctx := st.stopCtx + st.mu.Unlock() + if ctx != nil { + return ctx + } + } + return context.Background() +} + +// instanceFor picks the instance a resolution uses. A singleton has one for +// the whole binding; a Scoped binding has one per scope that holds it. +func (st *state) instanceFor(b *binding) *instance { + if !b.scoped { + return b.single + } + st.mu.Lock() + defer st.mu.Unlock() + in := st.scoped[b] + if in == nil { + in = &instance{b: b} + st.scoped[b] = in + } + return in +} + +// instanceAt is instanceFor without the making: the instance b already has +// in st, or nil. Called with st's mutex held, by the callers that must not +// bring one into being -- a recorded edge, and the inspection API. +func (st *state) instanceAt(b *binding) *instance { + if !b.scoped { + return b.single + } + return st.scoped[b] +} diff --git a/teardown_test.go b/teardown_test.go index 16f614a..a4f35ce 100644 --- a/teardown_test.go +++ b/teardown_test.go @@ -617,6 +617,34 @@ func TestReview2PanickingStartHookIsAFailure(t *testing.T) { } } +// A panicking start hook is observed like a failing one: its EventStart is +// emitted with the panic as Err, as a panicking drain or stop hook's already +// was. It was not, because start called the hook directly rather than through +// callHook, so the panic skipped the emit and observers saw a service that +// was built and then never heard of again. Found by the September 2026 +// code-organisation review and checked against 80895d2. +func TestPanickingStartHookIsObserved(t *testing.T) { + var events []di.Event + root := di.New() + root.Observe(func(ev di.Event) { + if ev.Kind == di.EventStart { + events = append(events, ev) + } + }) + root.Provide(func(*di.Scope) *DB { return &DB{} }). + OnStart(func(context.Context, *DB) error { panic("boom") }). + Eager() + if err := root.Start(context.Background()); err == nil { + t.Fatal("Start succeeded with a panicking OnStart") + } + if len(events) != 1 { + t.Fatalf("observed %d start events, want 1", len(events)) + } + if ev := events[0]; ev.Err == nil || !strings.Contains(ev.Err.Error(), "boom") { + t.Fatalf("the start event carries %v, want the panic", ev.Err) + } +} + // A hook that panics must not take the teardown down with it. The start step // was always recovered this way; the drain and stop steps were not, so a panic // in either propagated out of Stop halfway through -- stopOnce claimed and