2026-03-18 23:49:12 +00:00
|
|
|
# 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
|
|
|
|
|
|
2026-03-19 06:55:34 -06:00
|
|
|
**Tier:** 2 (depends on Tier 1 `logz`)
|
2026-03-18 23:49:12 +00:00
|
|
|
**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.
|