Files
launcher/CLAUDE.md
Rene Nochebuena 647810e4f7 docs(launcher): correct tier from 5 to 2
launcher only imports logz (Tier 1) — it belongs at Tier 2, not 5.
The wrong tier implied it had to be pushed last, when in reality worker,
postgres, mysql, sqlite, valkey, firebase, and httpserver all depend on it
and cannot be tagged before it.
2026-03-19 06:55:34 -06:00

115 lines
4.0 KiB
Markdown

# launcher
Application lifecycle manager: init, wire, start, wait, and graceful shutdown.
## Purpose
`launcher` orchestrates the startup and shutdown of all infrastructure components
(database pools, HTTP servers, background workers) in a Go service. It enforces a
strict phase order — `OnInit``BeforeStart` hooks → `OnStart` → wait for signal
`OnStop` in reverse — so that components are never started before their
dependencies are ready and never stopped before the components that depend on them.
## Tier & Dependencies
**Tier:** 2 (depends on Tier 1 `logz`)
**Imports:** `context`, `os`, `os/signal`, `sync`, `syscall`, `time` (stdlib);
`code.nochebuena.dev/go/logz` (micro-lib)
**Must NOT import:** `xerrors`, `rbac`, or any domain/application module.
`launcher` is the composition root; it should not depend on what it orchestrates.
## Key Design Decisions
- Three-phase lifecycle (`OnInit` / `BeforeStart` / `OnStart`) with reverse-order
shutdown. See `docs/adr/ADR-001-three-phase-lifecycle.md`.
- Shutdown runs in reverse registration order; each component gets an independent
per-component timeout (default 15 s). See `docs/adr/ADR-002-reverse-order-shutdown.md`.
- `BeforeStart` hooks run after all inits, before all starts — the correct place for
dependency injection wiring. See `docs/adr/ADR-003-before-start-hooks.md`.
- No singletons: `New(logger, opts...)` returns a `Launcher` interface; there is no
package-level instance, no `sync.Once`, no global state.
- `Shutdown(ctx)` is idempotent — safe to call from multiple goroutines.
## Patterns
**Basic wiring:**
```go
logger := logz.New(logz.Options{JSON: true})
lc := launcher.New(logger)
lc.Append(db, cache, server)
lc.BeforeStart(func() error {
return server.RegisterRoutes(db, cache)
})
if err := lc.Run(); err != nil {
logger.Error("launcher failed", err)
os.Exit(1)
}
```
**Custom component stop timeout:**
```go
lc := launcher.New(logger, launcher.Options{
ComponentStopTimeout: 30 * time.Second,
})
```
**Implementing the Component interface:**
```go
type DBClient struct { pool *sql.DB }
func (d *DBClient) OnInit() error { d.pool, err = sql.Open(...); return err }
func (d *DBClient) OnStart() error { return nil } // no goroutines to start
func (d *DBClient) OnStop() error { return d.pool.Close() }
```
**Programmatic shutdown (e.g. from a test):**
```go
go func() { lc.Run() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
lc.Shutdown(ctx)
```
**Registration order matters:**
```go
// Register dependencies before dependents
lc.Append(db) // stopped last
lc.Append(cache)
lc.Append(server) // stopped first (reverse order)
```
## What to Avoid
- Do not perform dependency injection wiring in `OnInit` or `OnStart`. Use
`BeforeStart` hooks; `OnInit` has no guarantee that other components are ready,
and `OnStart` is too late (components may already be serving).
- Do not create a package-level `Launcher` variable. `New()` is the only
constructor; call it from `main`.
- Do not call `os.Exit` inside a `Component` method. Return errors; let `main`
decide whether to exit.
- Do not register components after calling `Run`. `Append` and `BeforeStart` are
not safe to call concurrently with `Run`.
- Do not rely on `ComponentStopTimeout` as a substitute for proper `OnStop`
implementation. A timed-out stop means resources may leak.
## Testing Notes
- `compliance_test.go` asserts at compile time that `New(logz.New(Options{}))` returns
a value satisfying `launcher.Launcher`.
- `launcher_test.go` covers the full lifecycle: successful run + programmatic shutdown,
`OnInit` failure aborting before `OnStart`, `OnStart` failure triggering `stopAll`,
`BeforeStart` hook failure, reverse-order shutdown verification, and per-component
stop timeout.
- Tests use a lightweight `mockComponent` struct implementing `Component` with
controllable error injection and call-order recording.
- Run with plain `go test` — no external dependencies.