Files
launcher/CLAUDE.md

115 lines
4.0 KiB
Markdown
Raw Permalink Normal View History

# 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.