123 lines
4.1 KiB
Markdown
123 lines
4.1 KiB
Markdown
|
|
# 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
|