Documentation
¶
Overview ¶
Package app provides the top-level orchestrator that ties every lifecycle.Runner and lifecycle.Resource registered by a service into a single managed lifecycle: signal-aware startup, errgroup-based run, and two-phase LIFO shutdown with a bounded timeout.
A typical main.go looks like:
func main() {
a := app.New(app.WithShutdownTimeout(30 * time.Second))
a.Add(otelProvider, db, redisClient) // Resources
a.Add(grpcServer, httpServer) // Runners
if err := a.Run(); err != nil {
log.Fatal(err)
}
}
See the package README.md for the full semantics of the two-phase lifecycle and the rationale for LIFO shutdown ordering.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is the top-level lifecycle orchestrator. It collects Runner and Resource components via Add, starts all Runners when Run is called, and shuts them down in reverse registration order when the context is cancelled (typically by SIGINT/SIGTERM).
App is safe for single-goroutine use during setup. Add must not be called after Run — doing so is a programming error and will panic.
func New ¶
New creates an App with the given options. Defaults:
- Logger: slog.Default()
- Shutdown timeout: 30 seconds
- Signals: SIGINT, SIGTERM
func (*App) Add ¶
Add registers one or more components. Each argument must implement lifecycle.Runner or lifecycle.Resource. Runners go to the runners slice (Run + Shutdown); pure Resources go to the resources slice (Shutdown only). Since Runner embeds Resource, the type switch checks Runner first to avoid double-registration.
Add panics if a component implements neither interface — this indicates a programming error at startup and there is no reasonable recovery. The panic message includes the component's type for easy diagnosis.
Add must not be called after Run. Calling it concurrently with Run is undefined behavior.
Prefer AddRunner and AddResource when the component's role is known at the call site — they give compile-time type safety.
func (*App) AddResource ¶
AddResource registers one or more Resources. Equivalent to Add with compile-time type safety — the compiler rejects non-Resource arguments.
Note: a lifecycle.Runner is also a lifecycle.Resource (Runner embeds Resource), so passing a Runner here registers it as Resource-only — its Run method will NOT be invoked. Use AddRunner when Run is required.
Must not be called after Run; panics otherwise.
func (*App) AddRunner ¶
AddRunner registers one or more Runners. Equivalent to Add with compile-time type safety — the compiler rejects non-Runner arguments.
Must not be called after Run; panics otherwise.
func (*App) Healthcheck ¶
Healthcheck aggregates every registered component that implements lifecycle.Healthchecker. It returns nil if all checkers report healthy, or errors.Join of every failing checker's error otherwise.
Typical use: mount this on /readyz inside an http2.Server via http2.WithHealthcheckFrom(a).
Components that do not implement lifecycle.Healthchecker are silently skipped — they contribute neither success nor failure to the result.
Healthcheck is safe to call concurrently and at any time, including after Run has returned. It does not take App-level locks — checkers are iterated from the slices populated at startup.
func (*App) Logger ¶
Logger returns the *slog.Logger configured via WithLogger, or slog.Default() if none was set. Components that want to emit lifecycle-scoped logs can retrieve the app's logger here.
func (*App) Run ¶
Run starts every registered Runner concurrently, installs signal handlers, and blocks until:
- A signal from WithSignals is received, OR
- A Runner's Run method returns a non-nil error, OR
- All Runners complete normally (rare — typically only in tests).
On any of these, Run begins the shutdown phase:
- A fresh context with WithShutdownTimeout deadline is created.
- Runners have Shutdown called in reverse registration order (so frontends stop accepting traffic before backends are closed).
- Resources then have Shutdown called in reverse registration order.
- All shutdown errors, plus the original run error (if any), are joined and returned via errors.Join.
A context.Canceled from the first phase is treated as normal termination and not included in the returned error.
type Option ¶
type Option func(*App)
Option configures an App. Options are applied in order by New.
func WithLogger ¶
WithLogger injects a *slog.Logger that App will use for lifecycle events (startup, shutdown order, errors). As a convenience it also calls slog.SetDefault so third-party libraries and the stdlib log package route through the same handler.
If no logger is provided, App uses slog.Default().
func WithShutdownTimeout ¶
WithShutdownTimeout overrides the default 30-second shutdown budget. The timeout bounds the total time allowed for the shutdown phase — Runners and Resources share this single deadline, applied via context.WithTimeout in Run.
Setting a timeout shorter than any individual component's graceful-stop duration will cause that component to be force-closed — this is the correct behavior for bounded-duration shutdowns, but callers should size the timeout accordingly.
func WithSignals ¶
WithSignals overrides the default signal set (SIGINT, SIGTERM) that triggers App shutdown. Pass an empty slice to disable signal-driven shutdown entirely — the app will then only stop when Run's context (if supplied via an external mechanism) is cancelled.