myxce/.kiro/specs/election-iomgr-refactor/requirements.md
Rongsong Shen e31673dd88 chore: checkpoint all prior work for clean state
Captures all uncommitted work from ha-config-bootstrap,
ha-heartbeat-liveness, and associated spec/tooling updates:

- src: ha.c, ha.h, cluster.c, config_repl.c, myapp.c, raft.c —
  heartbeat liveness, RDHUP integration, committing phase fixes
- tests: test_ha_role.c, test_config_repl.c, test_ha_rdhup_integration.c,
  test_p1_integration.c, test_p3_hardening.c, scheduler test
- specs: election-iomgr-refactor, ha-heartbeat-liveness,
  ha-peer-connection-reuse, raft-library (new); ha-config-bootstrap
  and election-iomgr-refactor task/research updates
- tooling: .wolf/ state, ha/data cluster fixtures, raft/raft.tla,
  src/build symlink, xce-build skill update
2026-06-04 13:04:16 +00:00

10 KiB
Raw Blame History

Requirements Document

Introduction

The XCE controller operates a 3-node HA cluster where one node is elected PRIMARY before serving jobs. The current election mechanism mixes blocking per-peer pthreads with the iomgr epoll event loop, causing SSL context confusion, capnp use-after-free, and race conditions.

This feature replaces the blocking pthread-based election with a fully async iomgr-driven implementation, and formalizes the complete HA protocol: election by config freshness, replication push after primary determined, and availability gate after majority ack.

Boundary Context

  • In scope: Outgoing vote RPC transport; SSL context lifecycle for vote connections; election quorum; role-based vote grant/deny; config+state replication push to standbys after election; availability gate (majority ack before serving traffic).
  • Out of scope: Incoming vote RPC handling (already correct in cluster.c); dynamic cluster membership changes (candidate list is static); config schema changes.
  • Adjacent expectations: iomgr started and listener registered before election begins. The FSM state machine and ha_dispatch_gate remain the authority on opcode routing.

Requirements

1. No Blocking Threads for Outgoing Vote RPCs

Objective: As the cluster operator, I want the controller election to complete reliably without SSL errors or memory corruption, so that the cluster reaches PRIMARY state on every startup.

Acceptance Criteria

  1. The controller shall not create pthreads whose sole purpose is to send a vote request and block waiting for a reply during election.
  2. When the controller initiates an election round, the controller shall send OP_requestVote to each peer using the same I/O mechanism used for all other outgoing RPCs.
  3. If a peer does not respond within the election probe timeout, the controller shall treat that peer as a denied vote without stalling the event loop.

2. Async Outgoing Vote Connection via iomgr

Objective: As the controller process, I want outgoing vote connections managed by the iomgr event loop, so that SSL handshake and data transfer do not block other I/O.

Acceptance Criteria

  1. When the controller sends a vote request to a peer, the controller shall establish the TCP connection and complete the TLS handshake without blocking the main event loop thread.
  2. The controller shall use a TLS client context (not a server context) for outgoing vote connections.
  3. When the TLS handshake with a peer completes, the controller shall send the OP_requestVote packet through the established connection.
  4. If the TCP connection or TLS handshake to a peer fails, the controller shall count that peer as a denied vote and continue the election with the remaining peers.

3. Vote Reply Handling Through iomgr Dispatch

Objective: As the controller process, I want vote replies to arrive as normal iomgr packets, so that the capnp segment lifetime is managed consistently with all other RPCs.

Acceptance Criteria

  1. When a peer sends a vote reply, the controller shall decode it through the standard iomgr packet-receive path rather than a raw blocking read.
  2. The controller shall not access capnp-decoded fields after the capnp segment for that packet has been freed.
  3. When a vote reply is received, the controller shall extract the peer's term, vote_granted, peer_config_tag, and peer_last_log_lsn and update the election tally.
  4. If the peer's term in a vote reply is greater than the candidate's current term, the controller shall update its own term before recording the vote result.

4. Election Quorum, Ground-Truth, Timeout, and Retry

Objective: As the cluster operator, I want the election to converge to a single PRIMARY that holds the ground-truth cluster state, so that no data is lost and the cluster becomes available even if nodes start at different times.

Acceptance Criteria

  1. When vote replies from a majority of candidate nodes (≥ ⌈(n+1)/2⌉) grant the vote, the controller shall transition to CTRL_PRIMARY_COMMITTING (pre-availability state).
  2. The controller shall grant a vote to a candidate only if the candidate's config_tag is greater than or equal to the voter's own config_tag. This freshness gate guarantees that the elected primary holds the ground-truth configuration — no post-election config fetch is ever needed by the winner.
  3. On cold start, only the node holding the highest config_tag can win election. All other nodes (config_tag=0 on first boot) will lose the freshness gate and cannot become primary.
  4. When all peers have replied but no majority granted the vote, the controller shall retry after the next heartbeat timeout.
  5. When no vote replies arrive within the election probe timeout, the controller shall complete the round as a split vote and retry.
  6. While in CTRL_ELECTING state, if a heartbeat is received from a node that has become PRIMARY, the controller shall transition to CTRL_STANDBY and abandon the election.
  7. The controller shall retry elections indefinitely until it wins a quorum or detects an existing primary via heartbeat.

5. FSM State Consistency

Objective: As the controller process, I want the FSM state to accurately reflect the controller's role, so that the dispatch gate correctly accepts or rejects incoming RPCs.

Acceptance Criteria

  1. While in CTRL_ELECTING state, the controller shall accept OP_requestVote and OP_probeConfigTag and shall reply with XCE_ERR_NOT_READY to all other opcodes.
  2. While in CTRL_PRIMARY_COMMITTING state, the controller shall accept replication-related opcodes (config push ack, WAL ack) and shall reply with XCE_ERR_NOT_READY to job dispatch and queue management opcodes.
  3. When the controller transitions to CTRL_PRIMARY (fully available), it shall begin accepting job dispatch and queue management opcodes.
  4. When the controller transitions to CTRL_STANDBY, it shall begin accepting replication and heartbeat opcodes.
  5. The controller shall maintain a monotonically increasing term counter across election rounds and persist it to disk so the term survives a process restart.

6. Vote-Granting Rules Based on Voter Role

Objective: As the cluster operator, I want votes granted only when the voter has no live primary, so that a healthy primary is never displaced, and a crashed primary triggers a new election only after the heartbeat timeout expires.

Acceptance Criteria

  1. While in CTRL_PRIMARY state, the controller shall deny all incoming OP_requestVote requests regardless of the candidate's term or config_tag.
  2. While in CTRL_STANDBY state with a live primary connection (heartbeat received within heartbeat_timeout_ms), the controller shall deny all incoming OP_requestVote requests.
  3. When in CTRL_STANDBY state and the primary heartbeat has not been received within heartbeat_timeout_ms, the controller shall transition to CTRL_ELECTING and begin granting votes to eligible candidates.
  4. While in CTRL_INIT or CTRL_ELECTING state, the controller shall evaluate incoming OP_requestVote requests using the term, voted_for, and freshness gates (config_tag, last_log_lsn) without any role-based denial.
  5. When a primary crashes (stops sending heartbeats), the surviving standbys shall detect the loss within heartbeat_timeout_ms, transition to CTRL_ELECTING, and elect a new primary from among themselves using the last committed state.

7. Post-Election Config and State Replication

Objective: As the cluster operator, I want the elected primary to immediately propagate its ground-truth configuration and cluster state to all standbys, so that standbys have up-to-date state and can become primary on failover without any post-election config fetch.

Acceptance Criteria

  1. When the controller wins election and enters CTRL_PRIMARY_COMMITTING, it shall push the cluster configuration to all peer controllers that have a lower config_tag.
  2. When the controller wins election, it shall replicate its WAL (committed job/queue/worker state) to all standbys via the existing catch-up replication mechanism.
  3. When a new standby starts with config_tag=0 and loses the election, it shall receive the cluster configuration via the primary's replication push — it shall NOT attempt an independent config fetch before the primary pushes it.
  4. The controller shall confirm config and state receipt from each standby via acknowledgement before counting that standby toward the majority ack threshold.

8. Primary Availability Gate

Objective: As the cluster operator, I want the primary to begin serving job traffic only after the majority of controllers have confirmed they hold the replicated state, so that failover can always promote a standby with current data.

Acceptance Criteria

  1. After winning election, the controller shall enter CTRL_PRIMARY_COMMITTING and shall not accept job dispatch or queue management requests until the availability gate opens.
  2. When a majority of peer controllers (≥ ⌈(n+1)/2⌉ − 1 peers, since primary counts itself) have acknowledged receipt of the current config and committed WAL state, the controller shall transition from CTRL_PRIMARY_COMMITTING to CTRL_PRIMARY.
  3. When CTRL_PRIMARY state is reached, the controller shall begin accepting all client opcodes including job dispatch, queue control, and resource management.
  4. If the majority ack threshold cannot be reached within a configurable timeout, the controller shall log a warning and open anyway (degraded mode) so the cluster does not deadlock when standbys are unavailable at startup.

9. No Regression in Existing Behaviors

Objective: As the cluster operator, I want all existing behaviors preserved after the refactor, so that nodes that previously worked correctly continue to do so.

Acceptance Criteria

  1. When a single-node cluster starts (candidate count = 1), the controller shall self-elect to CTRL_PRIMARY without contacting any peers.
  2. The elected primary shall never need to fetch config from another node — the vote freshness gate guarantees it already holds the highest config_tag.
  3. When the primary's control channel connection drops (RDHUP), the controller shall trigger a new election. The winning standby shall promote via ha_promote_to_primary (hooks only — its state is already current via continuous replication).
  4. The controller shall prioritize election by node priority index: lower-index nodes start campaigning first, reducing split votes on simultaneous startup.