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:
122
README.md
Normal file
122
README.md
Normal 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
|
||||
Reference in New Issue
Block a user