feat(launcher): initial stable release v0.9.0
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/
This commit is contained in:
114
CLAUDE.md
Normal file
114
CLAUDE.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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:** 5 (application bootstrap only)
|
||||
**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.
|
||||
Reference in New Issue
Block a user