From f428fe9fde54915b248774e62c67143b289eecc9 Mon Sep 17 00:00:00 2001 From: Vsevolod Strukchinsky Date: Thu, 10 Sep 2026 17:29:01 +0500 Subject: [PATCH] Library: split di.go into six files and dedupe the hooks di.go was 2447 lines, a third of them comments, with the instance lifecycle 1200 lines away from the Start and Stop that drive it. The library is now di.go (package doc, keys, events, Scope, modules, Test), binding.go (registration), state.go (the registry and freeze), resolve.go (the resolution path, both cycle detectors, the build step), lifecycle.go (the phase machine, hooks, Start and Stop) and run.go (Run and Shutdown). Code moved verbatim; the edits came after. Simplifications: one state.report helper replaces six hand-built event lines; Wire and Wrap share the signature check, parameter keys and argument loop; instance.paired and instance.owes are the one statement of the owed predicate for both the drain and stop steps; once.wait returns its error last; two unreachable branches are gone (a nil start context, an eager key missing from the index). One behaviour fix rode along. instance.start called OnStart directly rather than through callHook, so a panicking start hook emitted no EventStart while a panicking drain or stop hook emitted its event with the error. It now goes through callHook, which also removes the duplicate recover block in startClaimed. TestPanickingStartHookIsObserved pins it and fails against 80895d2 with "observed 0 start events, want 1". Comments: the history told as narrative, the five restatements of the no-Stop-from-a-hook rule and the comments that restated code are gone; the invariant comments and the public godoc stay. CLAUDE.md, docs/DESIGN.md and the explain.go header describe the new layout. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 9 + CLAUDE.md | 48 +- binding.go | 396 +++++++++ di.go | 2145 +--------------------------------------------- docs/DESIGN.md | 7 +- explain.go | 2 +- lifecycle.go | 820 ++++++++++++++++++ resolve.go | 534 ++++++++++++ run.go | 126 +++ state.go | 241 ++++++ teardown_test.go | 28 + 11 files changed, 2198 insertions(+), 2158 deletions(-) create mode 100644 binding.go create mode 100644 lifecycle.go create mode 100644 resolve.go create mode 100644 run.go create mode 100644 state.go 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