Service-lifecycle supervisor for Go — concurrent startup, health probes, ordered (reverse-registration) shutdown, self-healing restarts
Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Docs: controls.go.phpboyscout.uk
gitlab.com/phpboyscout/go/controls — a small service-lifecycle supervisor
for long-running Go processes. Register your services (HTTP/gRPC servers,
background workers, schedulers), and the Controller starts them concurrently,
monitors their health (liveness/readiness), optionally takes ownership of
SIGINT/SIGTERM, and drives a bounded graceful shutdown in reverse
registration order — with an
optional self-healing restart policy. The shutdown bound covers stop callbacks
and supervisor exit: a context-ignoring stop — or a start that never returns —
is abandoned (and named in a WARN) at the deadline rather than wedging shutdown.
It is the same supervisor behind go-tool-base's service commands, extracted so any project can adopt it without pulling in the framework.
- Framework-free. The only external dependency is
cockroachdb/errors; the only logging seam is a nil-safe*slog.Logger. No config framework, no TUI, no OpenTelemetry, no go-tool-base. Adepfootprint_test.goguard enforces it. - Stdlib seams. Bring a
*slog.Logger(or none — it defaults to a discard handler). Everything else is functional options. - Correct concurrency. Idempotent
Start/Stop, a restart policy that distinguishes clean-exit / cancellation / failure and never floods the error channel, force-stop on shutdown timeout, and readiness that fails closed until the first async health check completes. Race-clean under-race.
go get gitlab.com/phpboyscout/go/controlspackage main
import (
"context"
"gitlab.com/phpboyscout/go/controls"
)
func main() {
c := controls.NewController(context.Background())
c.Register("api",
controls.WithStart(func(ctx context.Context) error {
// start serving; return when ctx is cancelled
<-ctx.Done()
return ctx.Err()
}),
controls.WithStop(func(ctx context.Context) {
// graceful shutdown, bounded by ctx
}),
)
c.Start() // launches the services and returns; installs no signal handler
c.Wait() // blocks until Stop (or the parent context) drains the shutdown sequence
}No OS signal handler is installed unless you pass controls.WithSignals() —
signal disposition is process-global, so it belongs to the outermost layer.
Controller— the supervisor.Registerservices beforeStart;Waitblocks until the full shutdown sequence completes and every supervisor goroutine has unwound (it requires context-respecting start callbacks —WaitContextis the deadline-bounded variant for services that may wrap cancellation-ignoring third-party code).- Health probes — attach
WithStatus/WithLiveness/WithReadinessto a service, or register standaloneHealthChecks (sync or async with anInterval).Status()/Liveness()/Readiness()return aggregateHealthReports for wiring into transport health endpoints. - Restart policy —
WithRestartPolicyenables self-healing with exponential backoff, aMaxRestartscap, a health-failure threshold, and a consecutive- failure counter that resets after a healthy window. - Options —
WithLogger,WithShutdownTimeout,WithSignals(standalone mains),WithValidError(exempt expected terminal errors likehttp.ErrServerClosedfrom the restart count).
Full guides and design notes: controls.go.phpboyscout.uk. API reference: pkg.go.dev.
See LICENSE.