diff --git a/.kiro/specs/ha-config-bootstrap/tasks.md b/.kiro/specs/ha-config-bootstrap/tasks.md index 96b4123..70cb115 100644 --- a/.kiro/specs/ha-config-bootstrap/tasks.md +++ b/.kiro/specs/ha-config-bootstrap/tasks.md @@ -10,7 +10,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A); --- - [ ] 1. Foundation: Wire protocol extensions and shared infrastructure -- [ ] 1.1 Add 6 new opcodes and message structs to controller.capnp +- [x] 1.1 Add 6 new opcodes and message structs to controller.capnp - Append `requestVote @32`, `probeConfigTag @33`, `walAck @34`, `upgradeConfigAck @35`, `snapshotTransfer @36`, `walCatchupEntry @37` after existing `upgradeConfig @31` - Add structs: `RequestVoteRequest`, `RequestVoteReply`, `ProbeConfigTagRequest`, `ProbeConfigTagReply`, `WalAckRequest`, `WalAckReply`, `UpgradeConfigAck`, `SnapshotTransfer`, `WalCatchupEntry` - Extend `CtrlHeartbeat`: add `committedLsn :UInt64` and `committedConfigTag :UInt64` fields @@ -20,7 +20,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A); - _Requirements: 2, 3, 5, 6, 7, 8_ - _Boundary: Wire Protocol (controller.capnp)_ -- [ ] 1.2 Add new enum values and types to ha.h +- [x] 1.2 Add new enum values and types to ha.h - Add `CTRL_INIT=2`, `CTRL_ELECTING=3` to `ctrl_role_t` (values above existing 0/1) - Add `ha_vote_entry_t` struct with fields: `peer`, `my_term`, `my_tag`, `my_lsn`, `vote_granted`, `replied`, `peer_tag`, `peer_lsn` - Add `ha_elect_ctx_t` struct with C99 flexible array `ha_vote_entry_t votes[]`; allocation comment: `calloc(1, sizeof(*ctx) + (n_peers-1) * sizeof(ctx->votes[0]))` diff --git a/src/controller/ha.h b/src/controller/ha.h index 19c4cd7..9e85d0b 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -19,6 +19,8 @@ */ #include +#include +#include #include #include #include @@ -51,10 +53,45 @@ void ha_seed_jitter(uint64_t *seed, int my_priority); /* ── Role enum ─────────────────────────────────────────────── */ typedef enum { - CTRL_PRIMARY = 0, - CTRL_STANDBY = 1 + CTRL_PRIMARY = 0, + CTRL_STANDBY = 1, + CTRL_INIT = 2, /* Startup: listener open, awaiting heartbeat or election timeout */ + CTRL_ELECTING = 3 /* Running quorum election via OP_requestVote */ } ctrl_role_t; +/* ── Election types ────────────────────────────────────────── */ + +/* + * ha_vote_entry_t: per-peer state accumulated by the election candidate. + * One entry per peer in ha_elect_ctx_t.votes[] (flexible array). + */ +typedef struct { + xce_candidate_t *peer; /* Peer being solicited */ + uint64_t my_term; /* Term sent in OP_requestVote */ + uint64_t my_tag; /* config_tag sent in request */ + uint64_t my_lsn; /* WAL LSN sent in request */ + int vote_granted; /* 1 if peer granted vote */ + int replied; /* 1 if peer replied (granted or denied) */ + uint64_t peer_tag; /* peer's config_tag from reply */ + uint64_t peer_lsn; /* peer's WAL LSN from reply */ +} ha_vote_entry_t; + +/* + * ha_elect_ctx_t: shared context for one quorum election round. + * Allocated as: calloc(1, sizeof(*ctx) + (n_peers-1) * sizeof(ctx->votes[0])) + * Stored in ha_state_t.elect_ctx (void * _Atomic) during election; NULL otherwise. + */ +typedef struct { + int n_peers; /* Total peer count (excluding self) */ + int votes_granted; /* Votes received; starts at 1 (self-vote) */ + int n_replied; /* Number of peers that replied */ + uint64_t max_resp_tag; /* Highest config_tag seen in any reply */ + uint64_t max_resp_lsn; /* Highest WAL LSN seen among max_resp_tag peers */ + int result; /* 1=primary, 0=standby, <0=error */ + sem_t done; /* Startup thread blocks; dispatch posts when done */ + ha_vote_entry_t votes[]; /* C99 flexible array: n_peers-1 entries */ +} ha_elect_ctx_t; + /* ── HA configuration ──────────────────────────────────────── */ /* Default values used by cluster_init(). Tuned for a LAN with ≤5ms RTT. */ @@ -108,6 +145,15 @@ typedef struct { void *ssl_ctx; /* SSL_CTX* cast; avoids pulling openssl headers here */ uint64_t jitter_seed; /* xorshift64 PRNG state for election jitter */ zarray_t *standby_endpoints; /* connected standby controller endpoints for config push */ + /* ── New fields added by ha-config-bootstrap spec ──────── */ + uint64_t voted_for_term; /* Req 9: term of last granted vote */ + char voted_for_name[XCE_MAXNAME_LEN]; /* Req 9: candidate of last granted vote */ + int primary_ctrl_fd; /* Req 6: persistent channel fd (-1 = none) */ + bool defer_config_fetch; /* Req 3: tag=0 node defers fetch to post-election */ + void * _Atomic elect_ctx; /* Req 2: ha_elect_ctx_t* during election; NULL otherwise */ + _Atomic uint64_t last_known_committed_lsn; /* Req 5: from primary heartbeats */ + sem_t heartbeat_seen; /* Req 1: posted by heartbeat handler during CTRL_INIT */ + uint64_t last_known_committed_config_tag; /* Req 8: from primary heartbeats */ } ha_state_t; /* ── Public API ────────────────────────────────────────────── */ @@ -231,6 +277,31 @@ void start_heartbeat_listener(cluster_t *c); */ void stop_heartbeat_listener(cluster_t *c); +/* ── HA Config Bootstrap & Term Durability (ha-config-bootstrap spec) ── */ + +/* + * ha_persist_term: write ha-term.bin atomically to {workdir}/ha-term.bin. + * Uses write + rename for durability (existing pattern in the codebase). + * Must be called before any vote is granted or term is incremented. + */ +void ha_persist_term(ha_state_t *ha, const char *workdir); + +/* + * bootstrap_config_if_needed: pre-election config probe. + * If the local config tag is 0 and at least one peer has config, sets + * ha->defer_config_fetch = true and returns without fetching. + * The actual fetch is deferred to post-election (after a winner is known). + * Returns 0 on success, -errno on unrecoverable error. + */ +int bootstrap_config_if_needed(cluster_t *c); + +/* + * xce_probe_config_tag: single-RTT OP_probeConfigTag RPC to one peer. + * Fills *tag_out with the peer's current config_tag on success. + * Returns 0 on success, -errno on connection/protocol error. + */ +int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out); + /* ── P3-6: Test-only failover shim ────────────────────────────── */ #ifdef XCE_TESTING