Documentation
¶
Overview ¶
Package runner runs the StandBy phase, starts and stops lifecycle resources wired via sdi.
After github.com/omcrgnt/sdi.Resolve, Runner receives []StandBy, []Starter and []Closer (registration order). Runner.Run runs the StandBy phase, then starts every Starter concurrently with a lifecycle context derived from the run context. The lifecycle context is canceled on starter failure (fail-fast) or when the parent run context is canceled; it is not canceled when Runner.Run returns after starters that exit Start without blocking (e.g. background servers).
StandBy.StandBy runs once per resource, sequentially, in registration order, before the Start phase — for zero-I/O finishing touches that read another resource's Inject-computed state (e.g. building an SDK client wrapper around a *clienthttp.Client whose own Inject ran later in the same sdi.Resolve pass). Anything that performs real I/O or may run long belongs in Starter instead.
Starter.Start must return promptly: spawn background work if needed and watch the lifecycle context there. Do not block inside Start until shutdown.
Starters run in two waves: every Starter that is not also a LastStarter first, concurrently, with no ordering between them; only once all of those have returned successfully does the second wave — every LastStarter — start, also concurrently with each other. Gate opens once both waves have fully succeeded, letting middleware/interceptors elsewhere (e.g. srv-http, srv-grpc) hold off real traffic until then; it never opens if either wave fails.
Both StandBy and Start return their own cleanup, a func(context.Context) error, in place of a separately-implemented Closer: whatever a resource sets up in StandBy or Start, it also tears down via the closure it hands back from that same call, not a second, independently-maintained method. This is why StandBy lives here rather than in a Bootstrap-scoped caller: Runner lives for the whole process, so it can retain that closure until Stop, unlike a closure local to a single Bootstrap call. Closer remains for resources with neither Start nor StandBy (e.g. client-http, client-s3, conn-sql, telemetry) — fully ready after Inject/Build, so there is no Start/StandBy call for them to return a cleanup from instead.
If a StandBy call fails, Run unwinds every earlier-succeeded StandBy's cleanup (reverse order) and returns the failure joined with any cleanup errors — the Start phase never begins. If a Starter's Start fails (either wave), Run unwinds every already-started Starter's cleanup (reverse order, concurrently) and every StandBy's cleanup (reverse order), joining all of that into the returned error.
Runner.Stop releases the lifecycle cancel func, then closes, in order:
- every started Starter's cleanup (reverse registration order, concurrently);
- every StandBy's cleanup together with every pure Closer (each reverse registration order; relative order between the two lists does not matter).
A cleanup already consumed by Run's own unwind on failure is not invoked again by a later Stop. Starters whose Start failed are not closed. There is no lifecycle dependency graph in sdi; close order is registration order only, not a reverse topo.
Register *Runner via github.com/omcrgnt/runner/use (Fixed on unique.Global). github.com/omcrgnt/app.App receives Runner through DI and calls Run/Stop.
Breaking v0.23: Runner now requires a gateOpener-compatible resource in the registry (see Gate) — sdi.Resolve fails without one. Real deployments get this for free: Gate registers itself on unique.Global via this package's own init, the same way Runner itself is typically registered. Only a hand-built registry (e.g. an isolated unique.New() in a test) needs to add one explicitly.
Breaking v0.24: Starter.Start returns (cleanup, error) instead of error; StandBy moved here from github.com/omcrgnt/app, and also returns (cleanup, error) instead of error.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Closer ¶
Closer is for a resource with neither Start nor StandBy — fully ready after Inject/Build (e.g. client-http, client-s3, conn-sql, telemetry) — so there is no Start/StandBy call for it to return a cleanup from instead.
type Gate ¶ added in v0.23.0
type Gate struct {
// contains filtered or unexported fields
}
Gate signals that both Starter waves have finished. Open is called once by Runner.Run after the second wave succeeds; Ready is a non-blocking check for anyone gating on it (e.g. srv-http/srv-grpc middleware, deciding whether to serve a request or answer 503/Unavailable).
type LastStarter ¶ added in v0.23.0
type LastStarter interface {
Starter
LastStart()
}
LastStarter is a Starter that must run only after every other Starter's Start has returned successfully — e.g. the ops/readiness server, whose own readiness checks read state that other Starters write during Start. Implement it by adding LastStart() (a marker; its body is never called) alongside the existing Start method. Run partitions r.starters by this marker locally — it is not requested as a separate sdi dependency, and any number of Starters may implement it (they run concurrently with each other in the second wave, same no-ordering-guarantee as the first).
type Runner ¶ added in v0.20.1
type Runner struct {
// contains filtered or unexported fields
}
Runner runs the StandBy phase, then starts Starter and stops Closer/Starter/StandBy cleanups, all injected via sdi after [Deps].
func (*Runner) Stop ¶ added in v0.20.1
Stop releases the lifecycle cancel func, then closes, in this order: every started Starter's cleanup (reverse registration order, concurrently), then every StandBy's cleanup together with every pure Closer (each reverse registration order; the two lists' relative order does not matter — StandBy/Closer resources were all ready before the Start phase began).
type StandBy ¶ added in v0.24.0
StandBy is a sequential post-Resolve initialization hook: it runs once, in registration order, before Run's Start phase, for zero-I/O finishing touches that read another resource's Inject-computed state (e.g. building an SDK client wrapper around a *clienthttp.Client whose own Inject ran later in the same sdi.Resolve pass). Anything that performs real I/O or may run long belongs in Starter instead.
On success, StandBy returns its own cleanup, called during Runner.Stop alongside pure [Closer]s — the same one-path rule as Starter: whatever a resource sets up in StandBy, it also tears down via the closure it hands back from StandBy, not a separately-implemented method. cleanup may be nil if there is nothing to undo (most StandBy resources, e.g. client-ollama, which only wraps an already-owned *http.Client, allocate nothing). On failure, cleanup is ignored (and must be nil).
Runner retains every returned cleanup for its own whole lifetime, not just for the duration of one Run call — this is why StandBy lives here rather than in a Bootstrap-scoped caller: a closure local to a single Bootstrap call cannot survive to be invoked by a later, ordinary Stop.
type Starter ¶
type Starter interface {
Start(ctx context.Context) (cleanup func(context.Context) error, err error)
}
Starter is a lifecycle resource that Runner starts during Run.
Start MUST return promptly after the resource is running (or ready to run in the background). It MUST NOT block until shutdown or wait on ctx.Done() as its main body. Long-running work belongs in a goroutine (or equivalent) that watches the lifecycle context passed to Start; the returned cleanup / context cancel stops that work.
Blocking inside Start breaks fail-fast and Stop: Runner only records a successful start after Start returns a nil error.
On success, Start returns its own cleanup, called during Runner.Stop in place of a separately-implemented Closer — a type that both starts and must be closed returns cleanup from Start instead of also implementing Closer, so there is exactly one path tying "how it was opened" to "how it is closed". cleanup may be nil if there is nothing to undo. On failure, cleanup is ignored (and must be nil).
Run starts every Starter concurrently, with no ordering guarantee within that wave — reading another normal Starter's Start-computed state from inside your own Start is unsafe. LastStarter is the one exception: it runs only after every normal Starter's Start has returned, so reading normal-Starter state from inside a LastStarter's Start is safe. See the lifecycle safety rule in this package's doc.