Controller reference¶
Controller is the supervisor: it owns the registered services, the control
goroutines, the lifecycle state and the shutdown sequence. This page lists its
constructor, its options and every method, with the behaviour of each when it is
called at the wrong time.
NewController — what it sets up¶
NewController allocates the controller and applies the options. It starts no
goroutine, installs no OS signal handler and touches no process-global state, so
constructing a controller you never start is harmless.
The ctx you pass is watched, not inherited. The controller derives its own
context with context.WithCancelCause(context.WithoutCancel(ctx)) and watches
ctx.Done() separately: completing your context — by cancel or by deadline —
triggers the ordinary shutdown sequence, so every service still observes
ErrShutdown as its context cause. Values on your context are preserved and
visible to services. See Cause
determinism.
State immediately after NewController, before any option is applied:
| Field | Value |
|---|---|
| Lifecycle state | Unknown |
| Logger | slog.New(slog.DiscardHandler) — nothing is logged |
| Shutdown timeout | DefaultShutdownTimeout (5s) |
| Message channel | unbuffered chan Message |
| Error channel | unbuffered chan error |
| Signal channel | nil — no OS signal handling |
| Valid-error predicate | none: every non-nil error from a StartFunc is a failure |
| Registered services / health checks | none |
Controller options¶
Each option is a ControllerOpt passed to NewController. There is no way to
apply one after construction that is safe once the controller is running — see
Setters.
| Option | Signature | Default when omitted | What it does, and what a bad value does |
|---|---|---|---|
WithLogger |
WithLogger(l *slog.Logger) |
discard handler | Stores l.With("component", "controller"). Passing nil panics with a nil pointer dereference inside NewController — omit the option instead if you want no logging. |
WithShutdownTimeout |
WithShutdownTimeout(d time.Duration) |
DefaultShutdownTimeout (5s) |
Bounds the whole stop phase, not each callback. A zero or negative d is not "use the default": it produces an already-expired budget, so stop callbacks are abandoned the instant they are launched and may never run at all. |
WithSignals |
WithSignals() |
no signal handling | Creates a signal channel with a one-slot buffer. signal.Notify for SIGINT/SIGTERM is registered in Start — not at construction — and detached again at shutdown. Do not use it under a CLI framework that already turns signals into cancellation. |
WithValidError |
WithValidError(fn func(error) bool) |
none | A StartFunc error for which fn returns true is classified as a normal end of run: not restarted, not counted, not forwarded on the error channel. Applies to every registered service; there is no per-service variant. |
Lifecycle methods¶
| Method | Blocks? | Effect | Called at the wrong time |
|---|---|---|---|
Start() |
no | Unknown → Running, then launches one supervisor goroutine per registered service, the async health-check goroutines, and the control goroutines (message processor, error/context handler, and the signal handler if WithSignals was passed). |
Any state other than Unknown: logs WARN "Start called, but controller is not in the Unknown state; ignoring" and returns. A controller is single-use — after Stopped it cannot be started again. |
Stop() |
no | Running → Stopping, then sends Stop on the message channel. Returns before the shutdown sequence has finished. |
Any state other than Running: logs WARN "Stop called, but not in expected state, unable to continue" and returns. Safe to call concurrently or repeatedly. |
Wait() |
yes, unbounded | Blocks until every supervisor goroutine has exited and the shutdown sequence has completed. | Before Start, the wait group is empty and it returns immediately. It never returns while a StartFunc refuses to return after cancellation — use WaitContext for that case. |
WaitContext(ctx) |
yes, bounded | Returns nil when the wait group drains, or ctx.Err() when ctx completes first. |
On the abandoned path the stuck supervisor goroutines and one internal helper goroutine are deliberately leaked. |
Wait and WaitContext are methods on the concrete *Controller only; they are
on no interface, including Controllable.
Registration methods¶
| Method | Returns | Rules |
|---|---|---|
Register(id string, opts ...ServiceOption) |
nothing | Must be called before Start. Names are not checked for uniqueness. Called after Start: logs WARN "Register called after Start; service will not be supervised", and the service is added to the collection but never started, stopped or supervised — while still appearing in every health report. See Services. |
RegisterHealthCheck(check HealthCheck) error |
error |
Must be called before Start. Returns cannot register health check after start in any other state, and duplicate health check name: "<name>" when a check of that name is already registered. Uniqueness is checked only against other health checks, never against service names. |
Health and introspection methods¶
| Method | Returns | Notes |
|---|---|---|
Status() |
HealthReport |
Every service (via its WithStatus probe) and every health check regardless of CheckType. Not a gate: an async check that has not run yet counts as OK. |
Liveness() |
HealthReport |
Every service (WithLiveness, falling back to WithStatus) plus checks of type CheckTypeLiveness or CheckTypeBoth. |
Readiness() |
HealthReport |
Every service (WithReadiness, falling back to WithStatus) plus checks of type CheckTypeReadiness or CheckTypeBoth. Fails closed: an async check with no result yet is reported ERROR. |
GetServiceInfo(name string) |
(ServiceInfo, bool) |
false when no service of that name was registered. When two services share a name, only the most recently registered one's info is stored. |
GetCheckResult(name string) |
(CheckResult, bool) |
false when the name is unknown or the check has not yet produced a result — which for a synchronous check means until a report including it has been built. |
GetState() |
State |
One of Unknown, Running, Stopping, Stopped. |
IsRunning(), IsStopping(), IsStopped() |
bool |
Equality tests against Running, Stopping, Stopped. There is no IsUnknown. |
GetContext() |
context.Context |
The controller's own context — the one services receive. It is cancelled with cause ErrShutdown during shutdown. |
GetLogger() |
*slog.Logger |
The configured logger, or the discard logger. |
WaitGroup() |
*sync.WaitGroup |
The wait group Wait blocks on. On no interface. |
All three reports are safe to call concurrently while the controller runs, and stay responsive during shutdown — the stop sequence does not hold the services mutex.
Channels¶
| Method | Channel | Default |
|---|---|---|
Messages() |
chan Message |
unbuffered |
Errors() |
chan error |
unbuffered |
Signals() |
chan os.Signal |
nil unless WithSignals was passed; buffered with one slot when it was |
Stop is the only Message the controller defines and the only one the message
processor acts on. Sending controls.Stop directly on Messages() drives the
same shutdown sequence as calling Stop().
The error channel's only receiver is the controller's error handler, which logs
each error at ERROR level — except context.Canceled, which it drops — and
exits when shutdown completes. Every internal
send is guarded against that exit, so a late error is dropped rather than
blocking a supervisor. If you replace the channel with SetErrorsChannel before
Start in order to consume errors yourself, you become that receiver and must
keep draining it.
Setters, and when they are safe to call¶
SetLogger, SetShutdownTimeout, SetErrorsChannel, SetMessageChannel,
SetSignalsChannel, SetWaitGroup and SetState mutate fields that the
control goroutines read after Start. They carry no internal synchronisation.
Call them only during construction, before Start — which is what the
WithX options do internally. Calling one on a running controller races the
running goroutines and is a programming error, not a supported reconfiguration
path. SetState in particular bypasses the compare-and-set transitions the
lifecycle depends on; it exists to satisfy the StateAccessor interface for
fakes, not to drive a real controller.
SetSignalsChannel detaches any previous signal.Notify registration before
storing the new channel, so swapping the channel cannot leave an orphaned
registration receiving signals nobody reads.
Package-level values¶
| Symbol | Value | Meaning |
|---|---|---|
ErrShutdown |
errors.New("controller shutdown") |
The cause attached to the controller context for every stop the controller drives. Test for it with errors.Is(context.Cause(ctx), controls.ErrShutdown). |
DefaultShutdownTimeout |
5 * time.Second |
Applied when WithShutdownTimeout is not passed. |
DefaultRestartResetInterval |
30 * time.Second |
Applied when RestartPolicy.RestartResetInterval is zero. |
Stop |
Message("stop") |
The only control message. |
Unknown, Running, Stopping, Stopped |
State values |
The lifecycle states, in order. |
Related¶
- Services and restart policy — what
Registeraccepts. - Defaults and timings — every default in one table.
- Handle graceful shutdown & signals — the shutdown sequence in order, and the signal-ownership rule.