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.
115 lines
4.0 KiB
Markdown
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.
|