Package reload owns SIGHUP-driven configuration reload. The coordinator runs a two-phase Check / Apply pass over a sequence of hooks, swaps the atomic config on success, and reports full / partial / validation / load outcomes via ReloadResult.
Coordinator owns the SIGHUP goroutine, the hook sequence, and the last-result snapshot. Construct it with New, then call Watch to start the signal listener. Shutdown stops the goroutine. LastResult is concurrent-safe.
typeCoordinatorstruct {
// contains filtered or unexported fields}
func New
funcNew(deps*Deps) *Coordinator
New returns a Coordinator with the given deps. Watch must be called to start observing SIGHUP. Deps is passed by pointer because it embeds a slog.LevelVar pointer and the runtime needs to mutate the underlying state through it.
func (*Coordinator) Generation
func (c*Coordinator) Generation() int64
Generation returns the monotonic generation counter. Starts at 0; advances on every successful Apply pass (full or partial).
func (*Coordinator) LastResult
func (c*Coordinator) LastResult() *ReloadResult
LastResult returns the most recent reload result, or nil if no reload has been attempted yet.
func (*Coordinator) Reload
func (c*Coordinator) Reload() *ReloadResult
Reload performs one full reload pass. Exposed so tests and admin surfaces can trigger a reload without sending a real signal. Returns the result of the pass; the same value is stored on the coordinator and reachable via LastResult.
func (*Coordinator) Shutdown
func (c*Coordinator) Shutdown()
Shutdown stops the SIGHUP goroutine and waits for it to exit. Safe to call multiple times.
func (*Coordinator) Watch
func (c*Coordinator) Watch()
Watch installs a SIGHUP handler and spawns the reload goroutine. The goroutine runs until Shutdown is called.
type Deps
Deps groups everything the coordinator mutates or reads on reload.
typeDepsstruct {
// ConfigPath is the on-disk YAML the SIGHUP handler reloads.ConfigPathstring// Injector is consulted by hooks for optional subsystems (rate// limiter, UI handler) that may not be registered in every mode.Injectordo.Injector// CfgPtr is the atomic config the rest of the process reads. The// coordinator swaps in the new config after a successful Apply pass.CfgPtr*syncutil.AtomicConfig[config.Config]
// LogLevel is updated by the log_level hook when Server.LogLevel// changes.LogLevel*slog.LevelVar// CertReloader rotates the TLS certificate; nil when TLS is off.CertReloader*httputil.CertReloader// Hooks overrides the default hook set. Production callers leave// it nil; tests use this to inject fakes.Hooks []Hook}
type Hook
Hook is the contract every reloadable subsystem implements. The coordinator runs Check on every hook first; any error aborts the pass before mutation. Apply then runs every hook, collecting per-hook outcomes. Apply errors mark the hook failed but do not abort the remaining hooks.
typeHookinterface {
// Name is a short stable identifier used in logs and outcomes.Name() string// Check validates the new config against the old one without// mutating any subsystem state. Returning an error aborts the// reload before any Apply fires.Check(oldCfg, newCfg*config.Config) error// Apply mutates the live subsystem. Returns HookApplied when the// mutation ran, HookSkipped when the subsystem is not configured// or not registered. Returning a non-nil error marks the hook// Failed regardless of the returned status.Apply(ctxcontext.Context, oldCfg, newCfg*config.Config) (HookStatus, error)
}
type HookOutcome
HookOutcome captures one hook’s contribution to a reload result.
typeHookOutcomestruct {
Namestring`json:"name"`StatusHookStatus`json:"status"`// Error is the rendered error message when Status is Failed.// Empty otherwise. Rendered (not error-typed) so the result is// JSON-serialisable for the admin reload-status endpoint.Errorstring`json:"error,omitempty"`}
type HookStatus
HookStatus describes the outcome of a single hook’s Apply call.
typeHookStatusstring
HookStatus values. Skipped covers “not configured / not registered” (e.g. UI handler when UI is disabled); Failed only fires when Apply returned a non-nil error.
ReloadResult is the aggregate report from a single reload pass. The coordinator stores the most recent result atomically; the admin API exposes it for operator inspection. Generation is monotonic and only advances on a successful Apply pass (FullSuccess or PartialApplied).
typeReloadResultstruct {
Generationint64`json:"generation"`StatusReloadStatus`json:"status"`// Outcomes lists every hook the coordinator considered, in apply// order. Skipped hooks are recorded too so operators can see which// subsystems were not part of the reload.Outcomes []HookOutcome`json:"outcomes"`// RequiresRestart lists non-reloadable config fields whose value// changed since the last successful load. Present regardless of// Status so operators always see the warning.RequiresRestart []string`json:"requires_restart,omitempty"`// LoadError is the rendered LoadConfig error when Status is// LoadFailed. Empty otherwise.LoadErrorstring`json:"load_error,omitempty"`StartedAttime.Time`json:"started_at"`EndedAttime.Time`json:"ended_at"`}
type ReloadStatus
ReloadStatus describes the overall outcome of a reload pass.
typeReloadStatusstring
ReloadStatus values:
FullSuccess: every hook returned Applied or Skipped.
PartialApplied: at least one hook returned Failed, but the Check pass succeeded so the config was still swapped in.
ValidationFailed: at least one hook’s Check returned an error; no Apply ran, no mutation happened, generation unchanged.
LoadFailed: the YAML file failed to load or validate; no hooks ran, generation unchanged.