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:
2026-03-18 23:49:12 +00:00
commit f2e3faa1d6
15 changed files with 1109 additions and 0 deletions

122
README.md Normal file
View File

@@ -0,0 +1,122 @@
# launcher
Application lifecycle manager for Go services.
| | |
|---|---|
| **Module** | `code.nochebuena.dev/go/launcher` |
| **Tier** | 2 — depends on `logz` |
| **Go** | 1.25 |
| **Dependencies** | `code.nochebuena.dev/go/logz` |
## Overview
`launcher` orchestrates a repeatable lifecycle sequence — init, assemble, start, wait, shutdown — across any number of registered infrastructure components. It does **not** manage dependency injection, provide a service locator, or define what your components are; it only drives them through a consistent set of phases.
## Installation
```sh
go get code.nochebuena.dev/go/launcher
```
## Quick start
```go
logger := logz.New(logz.Options{})
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)
}
```
## Usage
### Implementing `Component`
Any infrastructure piece implements the three-method interface:
```go
type Component interface {
OnInit() error // open connections, allocate resources
OnStart() error // start goroutines, begin serving
OnStop() error // stop and release all resources
}
```
### Lifecycle sequence
```
OnInit (all components, in registration order)
BeforeStart hooks (in registration order)
OnStart (all components, in registration order)
── application is running ──
OnStop (all components, in reverse registration order)
```
`Run` blocks at the "application is running" stage until SIGINT/SIGTERM is received or `Shutdown` is called. If any `OnInit`, hook, or `OnStart` returns an error, `Run` returns that error immediately (triggering `stopAll` for `OnStart` failures).
### `BeforeStart` hooks
Hooks run after all `OnInit` calls and before any `OnStart` call. Use them to wire dependencies that require all components to be initialized first:
```go
lc.BeforeStart(func() error {
// db and cache are both initialized at this point
return server.RegisterRoutes(db, cache)
})
```
### Programmatic shutdown
`Shutdown` triggers graceful shutdown and waits for `Run` to return. Use it in tests or when your application needs to shut down without an OS signal:
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := lc.Shutdown(ctx); err != nil {
// ctx expired before Run returned
}
```
`Shutdown` is idempotent — safe to call multiple times.
### Customising the stop timeout
```go
lc := launcher.New(logger, launcher.Options{
ComponentStopTimeout: 5 * time.Second,
})
```
The default is 15 seconds per component. The timeout controls how long `stopAll` waits for each individual `OnStop` call; it is independent of the `ctx` passed to `Shutdown`.
## Design decisions
- **`Run()` returns `error` instead of calling `os.Exit`.** The original implementation called `os.Exit(1)` directly, making the package untestable. Returning the error lets the caller (typically `main`) decide what to do.
- **`shutdownCh` + `doneCh` channel pair.** `shutdownCh` is closed by `Shutdown` to unblock `Run`'s wait loop. `doneCh` is closed by `Run` (via `defer`) before it returns. This guarantees that `Shutdown` unblocks even when `Run` returns early due to an error in `OnInit` or a hook.
- **Components stopped in reverse registration order.** This mirrors the LIFO teardown convention: the last component registered (typically the outermost layer, e.g. HTTP server) is stopped first, allowing inner layers (e.g. database) to remain available until no longer needed.
- **Per-component timeout, not a global one.** Each `OnStop` gets its own independent timeout window. A single slow component does not consume the budget for all others.
## Ecosystem
```
Tier 0 xerrors rbac
Tier 1 logz valid
Tier 2 ▶ launcher
Tier 3 postgres mysql sqlite valkey firebase httpclient worker
Tier 4 httputil httpmw httpauth-firebase httpserver
Tier 5 telemetry
```
`launcher` is the lifecycle backbone of the framework. Every Tier 3+ module implements `launcher.Component`; `Run()` drives them all through init → start → stop.
## License
MIT