Application lifecycle manager enforcing a three-phase init/wire/start sequence with reverse-order graceful shutdown and per-component stop timeouts. What's included: - `Component` interface (OnInit / OnStart / OnStop) and `Hook` type for BeforeStart wiring functions - `Launcher` interface with Append, BeforeStart, Run (blocks on SIGINT/SIGTERM), and idempotent Shutdown(ctx) - `New(logger, opts...)` constructor with configurable ComponentStopTimeout (default 15 s); no global state Tested-via: todo-api POC integration Reviewed-against: docs/adr/
49 lines
2.1 KiB
Markdown
49 lines
2.1 KiB
Markdown
# ADR-002: Reverse-Order Shutdown
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-03-18
|
|
|
|
## Context
|
|
|
|
When stopping an application, components must be stopped in the reverse of the order
|
|
they were started. A component that depends on another (e.g. an HTTP server that
|
|
uses a database connection pool) must be stopped before the component it depends on.
|
|
If the database pool were closed first, any in-flight request handled by the HTTP
|
|
server would fail with a connection error rather than completing gracefully.
|
|
|
|
## Decision
|
|
|
|
`stopAll` iterates `l.components` from the last index to the first:
|
|
|
|
```go
|
|
for i := len(l.components) - 1; i >= 0; i-- {
|
|
// stop l.components[i]
|
|
}
|
|
```
|
|
|
|
Components are typically registered in dependency order (dependencies first, then
|
|
dependents). Reverse-order shutdown therefore stops dependents before dependencies.
|
|
|
|
Each component's `OnStop` runs in its own goroutine. A `time.After` ticker enforces
|
|
the per-component `ComponentStopTimeout` (default 15 seconds). If a component does
|
|
not return within the timeout, the launcher logs an error and moves on to the next
|
|
component — it does not block the remainder of the shutdown sequence.
|
|
|
|
`Shutdown(ctx context.Context) error` provides a caller-side wait: it closes
|
|
`shutdownCh` (idempotent via `sync.Once`) and then blocks until `doneCh` is closed
|
|
(when `Run` returns) or until `ctx` is done. The caller's context controls how long
|
|
the caller is willing to wait for the entire shutdown; `ComponentStopTimeout`
|
|
controls how long each individual component gets.
|
|
|
|
## Consequences
|
|
|
|
- Shutdown order mirrors startup order in reverse, which is correct for any DAG of
|
|
component dependencies without requiring an explicit dependency graph.
|
|
- Each component's timeout is independent — a single slow component does not block
|
|
others from stopping.
|
|
- `Shutdown` is idempotent: calling it multiple times from multiple goroutines (e.g.
|
|
an OS signal handler and a health-check endpoint) is safe.
|
|
- If `ComponentStopTimeout` is exceeded, the component is abandoned — resources may
|
|
not be fully released. This is the correct trade-off for a graceful shutdown with
|
|
a deadline; the alternative (waiting indefinitely) is worse.
|