s3-orchestrator

reload

import "github.com/afreidah/s3-orchestrator/internal/reload"

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.

Index

type Coordinator

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.

type Coordinator struct {
    // contains filtered or unexported fields
}

func New

func New(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.

type Deps struct {
    // ConfigPath is the on-disk YAML the SIGHUP handler reloads.
    ConfigPath string
    // Injector is consulted by hooks for optional subsystems (rate
    // limiter, UI handler) that may not be registered in every mode.
    Injector do.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.

type Hook interface {
    // 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(ctx context.Context, oldCfg, newCfg *config.Config) (HookStatus, error)
}

type HookOutcome

HookOutcome captures one hook’s contribution to a reload result.

type HookOutcome struct {
    Name   string     `json:"name"`
    Status HookStatus `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.
    Error string `json:"error,omitempty"`
}

type HookStatus

HookStatus describes the outcome of a single hook’s Apply call.

type HookStatus string

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.

const (
    HookApplied HookStatus = "applied"
    HookSkipped HookStatus = "skipped"
    HookFailed  HookStatus = "failed"
)

type ReloadResult

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).

type ReloadResult struct {
    Generation int64        `json:"generation"`
    Status     ReloadStatus `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.
    LoadError string    `json:"load_error,omitempty"`
    StartedAt time.Time `json:"started_at"`
    EndedAt   time.Time `json:"ended_at"`
}

type ReloadStatus

ReloadStatus describes the overall outcome of a reload pass.

type ReloadStatus string

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.
const (
    ReloadFullSuccess      ReloadStatus = "full_success"
    ReloadPartialApplied   ReloadStatus = "partial_applied"
    ReloadValidationFailed ReloadStatus = "validation_failed"
    ReloadLoadFailed       ReloadStatus = "load_failed"
)

Generated by gomarkdoc