HA Operations — Rollout Leader Election and Streaming Promoter¶
This document covers the high-availability (HA) deployment model for the control-plane rollout subsystem.
Note: this page describes rollout evaluator leadership in the rollout store
path (orchestrator/rollout/*), which uses an advisory lease table. The
replicated PostgreSQL control-plane backend and epoch-fenced write authority
for the HA datastore path are documented in
self-hosted/tutorials/orchestrator-ha-runbook.md (orchestrator/pgstore/*).
Overview¶
The rollout promotion evaluator runs on a single leader node at any time.
Leadership is coordinated via a lease table (leader_lease) in the rollout
store.
HA semantics are advisory (spec Appendix D): no robot participates in
leader election, and no quorum is required for robot safety.
Two promoter instances run on each control-plane node:
Promoter |
Mode |
Frequency |
|---|---|---|
BatchPromoter |
Tick-based |
Every 10 seconds |
StreamingPromoter |
Event-driven |
Per EventBus event |
The BatchPromoter is the correctness fallback. If the StreamingPromoter misses events (leader failover, EventBus drops, process restart), the BatchPromoter catches up on its next tick.
Leader Lease Table¶
CREATE TABLE IF NOT EXISTS leader_lease (
id TEXT NOT NULL PRIMARY KEY DEFAULT 'promotion_leader',
holder TEXT NOT NULL,
acquired_at TEXT NOT NULL,
expires_at TEXT NOT NULL
);
Single-row table keyed on 'promotion_leader'. At most one row exists.
Election Protocol¶
Campaign (acquire/renew)¶
INSERT INTO leader_lease (id, holder, acquired_at, expires_at)
VALUES ('promotion_leader', ?, ?, ?)
ON CONFLICT(id) DO UPDATE
SET holder = excluded.holder,
acquired_at = excluded.acquired_at,
expires_at = excluded.expires_at
WHERE leader_lease.holder = excluded.holder
OR leader_lease.expires_at <= ?
A Campaign succeeds (RowsAffected > 0) when:
No lease row exists (first election), or
The caller already holds the lease (renewal), or
The existing lease has expired.
A Campaign fails (RowsAffected == 0) when another node holds an unexpired lease.
IsLeader (check)¶
Queries holder and expires_at from the lease row. Returns true when
holder == self.holderID AND now < expires_at. Results are cached for
1 second to reduce DB queries under high call rates.
Resign (voluntary release)¶
Deletes the lease row where holder == self.holderID. After Resign,
IsLeader returns false until a new Campaign succeeds.
Configuration¶
DBLeaderElectorConfig fields:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Shared SQLite database connection |
|
|
required |
Node identity (hostname or UUID) |
|
|
30s |
Lease duration |
Single-Node Deployment¶
For single-node deployments, use StaticLeaderElector — it always reports
leadership without touching the database. This is the default until HA is
explicitly enabled.
leader := cprollout.NewStaticLeaderElector()
Multi-Node Deployment¶
leader := cprollout.NewDBLeaderElector(cprollout.DBLeaderElectorConfig{
DB: db,
HolderID: hostname,
TTL: 30 * time.Second,
})
Each node calls Campaign(ctx, holderID) on startup. Only the winning node’s
StreamingPromoter processes events; the losing node’s handleEvent returns
immediately when IsLeader returns false.
Promotion Safety Under Leader Transitions¶
Monotonic promotion is guaranteed by the store’s PromoteStage query:
UPDATE stage_status SET phase = 'promoted' ... WHERE phase != 'promoted'
If two nodes briefly overlap (old leader’s last evaluation, new leader’s
first), the WHERE clause prevents double-promotion. The promoted stage
is never reverted.
Telemetry¶
Leader election does not emit its own telemetry events. The StreamingPromoter
and BatchPromoter emit rollout lifecycle events (ai.rollout.*) only when
they perform observable actions (stage promotion, rollback detection).
Failure Modes¶
Scenario |
Behavior |
|---|---|
Leader crashes without Resign |
Lease expires after TTL; another node acquires on next Campaign |
DB unavailable |
|
Both promoters miss a promotion |
Next BatchPromoter tick (≤10s) catches up |
Split-brain (both nodes think leader) |
Impossible with single SQLite DB; |