ADR-0004: One leader lease per background worker#
Status#
Accepted — April 2026
(Reconsideration tracked under
ROADMAP ## Consolidation & validation.
If the supervisor collapse lands, this ADR will be marked Superseded by ADR-XXXX.)
Context#
strata server ships roughly ten background workers — gc,
lifecycle, replicator, notify, access-log, inventory,
audit-export, manifest-rewriter, rebalance,
quota-reconcile, usage-rollup. Each of them must run on at most
one replica at a time: GC must not double-delete, lifecycle must
not duplicate transitions, etc. Some shape of singleton election is
required regardless of the deploy footprint.
The alternative shapes considered:
- Single “control” worker per replica, internally fanning out to every sub-worker. One global leader lease selects the active control replica; sub-worker scheduling lives inside the elected process.
- One leader lease per worker, independently elected. Each
worker on each replica races for its own
<name>-leaderlease vialeader.Session; the supervisor merely supervises the goroutines and recovers panics.
The first is simpler operationally (one chip to watch, one heartbeat) but tightly couples worker lifetimes: a panic in one worker pulls the control process down, lease-loss flips every worker at once, and one replica owns all background load.
Decision#
Each worker is leader-elected separately on a <name>-leader lease
via leader.Session. workers.Supervisor (in
cmd/strata/workers/) spawns one goroutine per requested worker;
each goroutine acquires its own lease, builds the runner under a
supervised context, runs it, releases on exit, and recovers from
panics with exponential backoff (1s → 5s → 30s → 2m, reset after 5
min healthy). Sharded fan-out workers (gc, rebalance) declare
SkipLease: true and own their per-shard <name>-leader-<i>
leases internally; the supervisor still owns panic recovery and
backoff, only the outer lease is skipped.
Consequences#
- Fault isolation. A panic in
notifydoes not affectgcor the gateway. Lease loss restarts the affected worker only — sibling workers and the HTTP listener keep serving. - Per-worker scaling knob.
STRATA_WORKERS=gc,lifecycleruns exactly those two workers on a replica; another replica can opt intoSTRATA_WORKERS=notify,access-log. Operators tune the load shape per replica without forcing a homogeneous fleet. The gateway-only deploy isSTRATA_WORKERS=(empty). - Independent observability per worker. The supervisor emits an
acquired/releasedheartbeat per worker (leader_for=<name>),strata_worker_panic_total{worker=<name>, shard=<i>}exposes panic rates per worker, and OTel iteration spans (worker.<name>.tick) carrystrata.worker=<name>for filtering. - Higher Cassandra LWT churn. Each lease is held via LWT-backed heartbeat; ten leases × N replicas multiplies the heartbeat traffic compared to a single global lease. In practice the heartbeat cadence is generous enough (seconds, not milliseconds) that the churn is invisible against the data-plane write rate — but it is a real cost that the consolidation reconsideration takes seriously.
- Reconsideration path. A future supervisor that owns a single
strata-control-leaderlease and dispatches to internally scheduled sub-workers is on the table. Migration would (a) flip theBuildconstructors to expose tick functions instead of long-lived runners, (b) replace the per-worker leases with one global lease and a deterministic in-process scheduler, and (c) retainSTRATA_WORKERS=as the enable mask. This ADR’sSuperseded byline is reserved for that change.