controls¶
A small service-lifecycle supervisor for long-running Go processes:
concurrent startup, health probes, ordered (reverse-registration) shutdown and
self-healing restarts, behind one Controller, as a light, framework-free
library.
controls is the same supervisor behind go-tool-base's
service commands, extracted so any project can register its services (HTTP and
gRPC servers, background workers, schedulers) and let the Controller start
them concurrently, aggregate their health, optionally take ownership of
SIGINT/SIGTERM, and drive a bounded graceful shutdown, without pulling
in the framework.
The light-footprint promise¶
The module graph is deliberately tiny. The library has exactly one external
dependency, go/errors, a sibling
module in the same toolkit; the others in go.mod are for tests. No config
framework, no TUI, no OpenTelemetry, no go-tool-base: depfootprint_test.go
fails the build if any module but go/errors ever enters the library's graph.
The only logging seam is the standard library's *slog.Logger, injected with
WithLogger. Supply one or supply none: with none the controller logs to a
discard handler, so logging is never mandatory and never a dependency you have
to adopt.
Design¶
- Framework-free. One external dependency (
go/errors); one logging seam (*slog.Logger). Everything else is functional options. - Correct concurrency. Idempotent
Start/Stop; goroutines that terminate on shutdown rather than leak or busy-spin; a restart supervisor that distinguishes a clean start from a cancellation from a failure; failures delivered to a callback or the error channel without ever blocking a service; a stop callback that overruns the shutdown deadline is abandoned rather than waited for, and the shutdown'sOutcomesays so; readiness fails closed until the first async health check has run. Race-clean under-race. - Transport-agnostic health.
Status(),Liveness()andReadiness()return plainHealthReportvalues. The module does not open a port or register HTTP or gRPC handlers. You wire the reports into whatever transport your process already speaks (see Add health checks).
Quick start¶
package 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
}
Add controls.WithSignals() to NewController when this process is the
outermost layer and Ctrl-C should stop it. See Handle graceful shutdown and
signals for why it is opt-in.
Key concepts¶
Controlleris the supervisor.Registerservices beforeStart;Waitblocks until the full shutdown sequence has completed and every supervisor goroutine has unwound.Waitrequires start callbacks that return when their context is cancelled;WaitContextis the deadline-bounded variant for a service that wraps third-party code which may not.- Health probes. Attach
WithStatus,WithLivenessorWithReadinessto a service, or register standaloneHealthChecks (sync, or async with anInterval).Status(),Liveness()andReadiness()aggregate them into aHealthReport. - Restart policy.
WithRestartPolicyenables self-healing with exponential backoff, aMaxRestartscap on consecutive failures, a health-failure threshold, and a counter that resets after a healthy window. Supervisormanages a set of children that attach and detach while the process runs, under the sameRestartPolicyrules. AControllermanages a fixed set with ordered shutdown; aSupervisormanages a changing one with concurrent shutdown, and a process usually wants both.Generationalowns one generation of a resource at a time, for a service that holds something single-use (a listener, a server, a supervisor) and has to survive a restart.- Options.
WithLogger,WithShutdownTimeout,WithSignalsfor a standalone main, andWithValidErrorto exempt an expected terminal error such ashttp.ErrServerClosedfrom the restart count.
Install¶
Where to go next¶
The documentation follows the Diátaxis framework:
- Tutorials are learning-oriented walkthroughs:
- Getting started: register one service, start it, and shut down cleanly on Ctrl-C.
- Supervise workers that come and go: add a set of workers that changes while the process runs, and watch a failed one leave the process ready.
- How-to guides are task-oriented recipes:
- Explanation is understanding-oriented background:
-
Reference lists every option, field and default, with what happens when it is set wrongly:
- Controller
- Services and restart policy
- Supervisor
- Generational
- Health checks and reports
- Defaults and timings
- Interfaces
The generated listing of every exported symbol, with runnable
Exampletests, is on pkg.go.dev.
Using go-tool-base? The framework wires this supervisor into its service commands and exposes the
HealthReports through its ownpkg/httpandpkg/grpchandlers. That glue lives in go-tool-base, not here; this module is transport- and framework-agnostic.
Further reading¶
The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.