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

4.0 KiB

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 — OnInitBeforeStart 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:

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:

lc := launcher.New(logger, launcher.Options{
    ComponentStopTimeout: 30 * time.Second,
})

Implementing the Component interface:

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 func() { lc.Run() }()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
lc.Shutdown(ctx)

Registration order matters:

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