Register & run services¶
This guide covers registering one or more services, the ordering guarantees during startup and shutdown, and what happens when you omit a lifecycle callback.
Register a single service¶
A service is a unique name plus lifecycle callbacks supplied as
ServiceOptions. The common shape wraps a real server:
c := controls.NewController(context.Background())
srv := &http.Server{Addr: ":8080", Handler: mux}
c.Register("http-api",
controls.WithStart(func(ctx context.Context) error {
// ListenAndServe blocks until the server is closed. Report
// ErrServerClosed as an expected terminal error, not a failure.
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}),
controls.WithStop(func(ctx context.Context) {
// ctx is bounded by the shutdown timeout; Shutdown drains in-flight
// requests until it elapses.
_ = srv.Shutdown(ctx)
}),
)
Blocking vs. background starts. A
WithStartmay either block for the service's whole lifetime (likeListenAndServeabove) or spawn a goroutine and returnnilimmediately. Both are supported. Anilreturn is treated as a clean start, not an exit — the supervisor keeps the service alive until shutdown. See The restart supervisor.
Give every service a distinct name¶
Register takes no error return and does not check names for uniqueness.
Registering two services as worker is accepted: both run, both appear in every
health report as separate entries named worker, and GetServiceInfo("worker")
can only return one of them — the one registered last. Nothing warns you. Use
distinct names.
The same applies across kinds: a standalone HealthCheck may share a name with a
service, because RegisterHealthCheck only checks for collisions among health
checks.
Register several services¶
Call Register once per service, all before Start:
c.Register("database", controls.WithStart(openDB), controls.WithStop(closeDB))
c.Register("http-api", controls.WithStart(serveHTTP), controls.WithStop(stopHTTP))
c.Register("worker", controls.WithStart(runWorker))
c.Start()
c.Wait()
Startup and shutdown ordering¶
- Startup launches a supervisor goroutine per service in registration
order. Services start concurrently — the controller does not wait for one
WithStartto return before launching the next (that is what lets a blocking server and a background worker coexist). - Shutdown runs each
WithStopin reverse registration order, one at a time. Registeringdatabasefirst andhttp-apisecond means the HTTP server is stopped before the database on the way down — dependencies you bring up first are torn down last.
Ordering is by registration, not by readiness. The controller does not model inter-service dependencies or block a start until a dependency is "ready". If service B must not begin work until service A is up, gate that inside B's
WithStart(for example, by having it wait on a channel A closes).
Omitting Start or Stop¶
Both callbacks are optional. A service registered without a WithStart defaults
to a no-op that returns nil; one without a WithStop defaults to a no-op stop.
Neither ever panics.
// A marker service that only reports health — no start/stop behaviour.
c.Register("readiness-gate",
controls.WithReadiness(func() error { return checkDependencies() }),
)
This is useful for a service whose only job is to contribute a probe to the aggregate health report, or as a placeholder during incremental development.
What happens if you register after Start¶
Register still accepts the service, logs
WARN "Register called after Start; service will not be supervised", and adds it
to the collection — but Start has already snapshotted the service set and
launched the supervisor goroutines, so that service's StartFunc is never
called, its StopFunc never runs, and no restart policy applies to it.
The trap is that it does appear in Status(), Liveness() and Readiness(),
reporting "OK" if it has no probe. A late registration therefore looks healthy
while doing nothing at all. Treat the warning as a bug in your wiring rather than
a caution.
Inspect a running service¶
GetServiceInfo returns runtime metadata for a registered service — its restart
count, last start/stop times, and last error:
info, ok := c.GetServiceInfo("worker")
if ok {
fmt.Printf("restarts=%d lastErr=%v\n", info.RestartCount, info.Error)
}
Related¶
- Add health checks — attach probes and standalone checks.
- Services and restart policy reference — every
option, the callback contracts, and every
RestartPolicyfield. - Configure restart policy — make a service self-healing.
- Architecture — how the supervisor goroutines and the state machine fit together.