18 KiB
Gap Analysis: ha-config-bootstrap
Generated: 2026-05-27. Branch: persistent_ha.
Analysis Summary
- Scope: 10 requirements across 8 distinct changes (A–H). The codebase has strong foundational infrastructure (iomgr, repl slot management, WAL, SSL) but the HA election core, quorum commit, and control channel are mostly absent or stubbed.
- Largest gaps: Raft-style quorum election (Req 2) and persistent control channel with full state sync (Req 6) are full rewrites/additions with no current equivalent logic; majority-commit WAL (Req 5) requires implementing the two stub functions at the heart of replication.
- Strong reusable assets:
repl_add_standby/remove/start_catchup/complete_catchup/needs_snapshotare real implementations; the SSL connect pattern (connect_ssl_raw), xorshift64 PRNG,ha_do_promote/demote, and iomgr epoll machinery are all directly reusable. - Risk concentration: Req 2 and Req 6 carry High risk; Req 5 carries High risk due to
committed_lsncross-cuttingpersist_t,repl.c, andha_state_t. The remaining requirements are Medium or Low. - Recommended approach: Hybrid (Option A) — implement changes A→D→B→C→E→F→G→H in dependency order, writing C tests first per TDD, one atomic commit per change.
1. Current State Investigation
1.1 HA Core (ha.h / ha.c)
ctrl_role_t enum — only two values: CTRL_PRIMARY = 0, CTRL_STANDBY = 1.
Missing: CTRL_INIT, CTRL_ELECTING (required by Req 1).
ha_state_t — current fields of interest:
| Field | Status | Notes |
|---|---|---|
term (_Atomic uint64_t) |
Exists | Used for fencing |
role (_Atomic int32_t) |
Exists | Cast to ctrl_role_t |
my_priority |
Exists | LSF priority index — kept for compat but unused in Raft |
applied_lsn (_Atomic uint64_t) |
Exists | Reusable for vote freshness check |
primary_host[XCE_MAXNAME_LEN] |
Exists | Reusable |
primary_port |
Exists | Reusable |
last_heartbeat (struct timespec) |
Exists | Replaces with EPOLLRDHUP path in Req 6 |
in_catch_up (_Atomic int) |
Exists | Reusable for Clarification 6 vote exclusion |
jitter_seed (uint64_t) |
Exists | xorshift64, directly reusable |
ssl_ctx (void*) |
Exists | Reusable |
standby_endpoints (zarray_t*) |
Exists | Superseded by per-standby fd table in Req 6 |
voted_for_term (uint64_t) |
MISSING | Req 9 |
voted_for_name (char[]) |
MISSING | Req 9 |
primary_ctrl_fd (int) |
MISSING | Req 6 |
defer_config_fetch (bool) |
MISSING | Req 3 |
elect_ctx (_Atomic ptr) |
MISSING | Req 2 |
last_known_committed_lsn (_Atomic uint64_t) |
MISSING | Req 5 |
heartbeat_seen (sem_t) |
MISSING | Req 1 |
ha_config_t — missing: commit_timeout_ms. All election fields present (election_timeout_ms, jitter_max_ms, etc.) but values tuned for LSF model.
am_i_primary() — LSF priority probe via per-peer pthreads. No quorum, no config_tag/LSN in vote, no term/voted_for persistence. Must be replaced entirely with am_i_primary_quorum().
ha_check_priority_stepdown() — permanently unreachable once term > 0; exists only for the term=0 simultaneous-boot race. Must be retired (Req 2).
ha_check_heartbeat_nocluster() — timestamp polling with configurable timeout. Replaced by EPOLLRDHUP fast path + heartbeat timeout vtask (Req 6). Function itself can be deleted.
ha_watchdog_thread() — sets failover_requested but cannot call ha_promote_to_primary() (no cluster_t). Partially functional stub. Replaced by vtask-based heartbeat monitor (Req 6).
start_heartbeat_listener() / stop_heartbeat_listener() — no-op stubs with comment "full implementation in cluster.c Task 7". Missing (Req 1).
ha_fini() — missing SSL_CTX_free(ha->ssl_ctx). ASAN-visible leak (~5 KB).
1.2 Startup Sequence (myapp.c / main.c)
myapp.c current order:
cluster_init_phase1()
initialize_openssl() + create_ssl_context()
am_i_primary() ← election here
load_primary_config() / fetch_config_from_primary()
cluster_init_phase2() ← iomgr, vtaskmgr, bind+listen inside
cluster_start() ← more network init
cluster_run()
Bind+listen happens inside cluster_init_phase2() or cluster_start() — after election. Gap: concurrent-startup nodes probe an unbound port (Req 1).
No bootstrap_config_if_needed() — standby path goes straight to fetch_config_from_primary() post-election without checking whether local config_tag is 0 or whether peers have config. Gap (Req 3).
main.c:
--vcpus Narg: MISSING. vcpus comes from cluster config read inside phase2. This creates a dependency that prevents vtaskmgr from running before election (Req 1, 4).- Peer count validation: MISSING. Even-N peer lists accepted silently (Req 4).
- Cert pre-flight
access()check: MISSING. Cert paths computed but SSL context created later in myapp.c, not with early-exit error in main.c (Req 4).
1.3 Replication (repl.c / repl.h)
| Function | Status | Notes |
|---|---|---|
repl_init / fini |
Real | Lifecycle ok |
repl_add_standby |
Real | Slot management implemented |
repl_remove_standby |
Real | Implemented |
repl_start_catchup |
Real | Sets in_catch_up=1 |
repl_complete_catchup |
Real | Clears in_catch_up, broadcasts cond |
repl_needs_snapshot |
Real | Compares standby_lsn vs oldest_wal_lsn |
repl_on_ack |
Real | Updates acked_lsn, signals cond |
repl_trim_lsn |
Real | WAL trim guard implemented |
repl_broadcast |
STUB | Returns 0 without sending anything (Req 5, 8) |
repl_wait_ack |
Partial | Waits for ANY non-catch-up standby, not majority (Req 5) |
repl_wait_majority_ack |
MISSING | Req 5, 8 |
standby_conn_t — missing pending_ack_count for backpressure (Req 5).
1.4 Persistence (persist.h / persist.c)
current_lsnandcheckpoint_lsnexist.committed_lsn— MISSING (Req 5).persist_wal_sync_append()usesrepl_wait_ack()(any-standby) — must upgrade torepl_wait_majority_ack().persist_truncate_wal_above()— MISSING (Req 5, 6).persist_config_t.sync_timeout_msexists — analogous tocommit_timeout_msneeded for majority-commit.
1.5 Config Replication (config_repl.c)
do_one_config_fetch()— wired (recent commit2aae45fon branch: "wire do_one_config_fetch() — full SSL roundtrip"). Not a stub.bootstrap_config_if_needed()— MISSING (Req 3).xce_probe_config_tag()— MISSING. New lightweight RPC needed (Req 3).push_config_to_standbys()— declared in config_repl.h; likely fire-and-forget (no majority wait). Gap: must upgrade to majority-commit (Req 8).OP_probeConfigTagCap'n Proto schema — MISSING (Req 3).OP_upgradeConfigAckCap'n Proto schema — MISSING (Req 8).committed_config_tag— MISSING (Req 8).
1.6 Control Channel (ha.c / cluster.c)
- No persistent standby-to-primary TCP connection. Each config fetch and WAL entry opens a new SSL connection via
connect_ssl_raw(). ha_find_and_connect_to_primary()— MISSING (Req 6).- Extended
OP_ctrlHellowith applied_lsn, config_tag — MISSING (Req 6). OP_snapshotTransfer,OP_walCatchupEntry— MISSING (Req 6).handle_ctrlhello()extended handler — MISSING (Req 6).- Three-tier primary discovery — MISSING (Req 6).
- EPOLLRDHUP failover detection — MISSING (Req 6).
- Heartbeat timeout vtask replacing watchdog thread — MISSING (Req 6).
1.7 Election State Durability
ha-term.binfile — does not exist.ha_persist_term()— MISSING (Req 9).ha_init()zeros term and has noha-term.binread path.
1.8 libxce Client Retry (src/lib/)
ha_should_redirect()/ha_no_primary_known()exist in ha.c and are used by join.c.- No transparent retry loop for
XCE_ERR_NOT_READY,XCE_ERR_STANDBY,ECONNREFUSED. xce_retry_config_t— MISSING (Req 7).redirect_host/redirect_portfields inXCE_ERR_STANDBYerror reply — MISSING (Req 7).
2. Requirement-to-Asset Map
| Req | Key Gap | Key Reusable Asset | Gap Tag |
|---|---|---|---|
| 1 — Listener before election | CTRL_INIT/CTRL_ELECTING enum values; pre-dispatch gate in vtaskmgr; extract bind+listen from phase2; ha_wait_for_heartbeat_or_timeout(); heartbeat_seen semaphore |
iomgr epoll, vtaskmgr dispatch switch, xorshift64 jitter | Missing |
| 2 — Quorum election | Entire am_i_primary_quorum() and vote protocol; ha_vote_entry_t, ha_elect_ctx_t; handle_request_vote() / _reply(); ha_elect_evaluate_and_signal(); OP_requestVote capnp schema |
ha_do_promote/demote, jitter PRNG, applied_lsn, iomgr async connect |
Missing |
| 3 — Config bootstrap | bootstrap_config_if_needed(); xce_probe_config_tag(); OP_probeConfigTag capnp; defer_config_fetch flag in ha_state_t |
do_one_config_fetch() (wired), connect_ssl_raw() |
Missing |
| 4 — Startup validation | --vcpus N CLI arg; even-N peer count check; cert pre-flight access(); SSL_CTX_free ASAN fix |
argp option parsing pattern in main.c | Missing |
| 5 — Majority-commit WAL | repl_broadcast() implementation; repl_wait_majority_ack(); committed_lsn in persist_t; persist_truncate_wal_above(); committed_lsn in heartbeat; last_known_committed_lsn in ha_state_t |
repl_wait_ack() logic, standby_conn_t, repl_start/complete_catchup, WAL sync path |
Missing / Stub |
| 6 — Persistent channel | Persistent TCP fd per standby; ha_find_and_connect_to_primary(); EPOLLRDHUP failover; extended OP_ctrlHello; OP_snapshotTransfer; OP_walCatchupEntry; WAL streaming loop; heartbeat vtask; three-tier discovery |
iomgr EPOLLIN/EPOLLRDHUP, repl_needs_snapshot(), repl_start/complete_catchup() |
Missing |
| 7 — Client retry | Retry loop in libxce; xce_retry_config_t; redirect fields in error reply |
ha_should_redirect(), ha_no_primary_known() |
Missing |
| 8 — Majority-commit config | repl_wait_majority_ack() (shared with Req 5); OP_upgradeConfigAck; committed_config_tag; heartbeat piggybacking |
push_config_to_standbys() (fire-and-forget → upgrade) |
Missing |
| 9 — State durability | ha_persist_term(); ha-term.bin read in ha_init(); voted_for_term/voted_for_name in ha_state_t |
Atomic write+rename pattern (existing in config_repl.c) | Missing |
| 10 — Observability | Structured log statements throughout all new code paths | Existing INFO/WARN/ERROR macros (xcelog.h) | Missing (alongside) |
3. Implementation Approach Options
Option A: Change-at-a-time (recommended)
Implement changes in dependency order: D → A → B → C → E → H → F → G → (observability woven in).
Rationale for ordering:
- D first:
--vcpus Nremoves vtaskmgr's config dependency, unblocking A. - A before B: listener must be open before election dispatch works.
- B before C: config bootstrap needs the proven election winner from quorum election.
- E and H together: both use
repl_broadcast()+repl_wait_majority_ack(); implement infrastructure once. - F after E: persistent channel carries WAL entries; broadcast must send over that channel.
- G last: retry makes sense only after the controller states it retries against are implemented.
Files touched per change:
- D:
main.c,cluster.c(remove vcpus from config),ha.c(SSL_CTX_free) - A:
ha.h(enum + semaphore + fields),myapp.c(startup reorder),cluster.c(extract listener),ha.c(wait function, dispatch gate) - B:
ha.h(new types),ha.c(replaceam_i_primary), capnp schema,cluster.c(dispatch handler) - C:
config_repl.c(bootstrap_config_if_needed,xce_probe_config_tag), capnp schema,myapp.c(integration) - E:
repl.c(repl_broadcast,repl_wait_majority_ack),persist.h/persist.c(committed_lsn,persist_truncate_wal_above),ha.h/ha.c(last_known_committed_lsn) - H:
config_repl.c(upgrade majority-commit), capnp schema,ha.h(committed_config_tag) - F:
ha.c/cluster.c(channel connect, extended hello, streaming, EPOLLRDHUP) - G:
src/lib/(retry loop, xce_retry_config_t, redirect fields)
Trade-offs:
- ✅ Each atomic commit is testable before the next starts
- ✅ Follows existing TDD convention (failing test first)
- ❌ Long sequential dependency chain; F and G can only start late
Option B: Infrastructure-first parallel tracks
Track 1 (election+bootstrap): D → A → B → C
Track 2 (replication): E → H
Join (channel): F using Track 1 + Track 2 artifacts
Final: G
Trade-offs:
- ✅ Track 1 and Track 2 are mostly independent until F
- ❌ Track 2 cannot be fully validated without Track 1 (no quorum without election)
- ❌ More coordination overhead between developers on shared ha.h
Option C: Stub-forward then wire
Add all new stubs with -ENOSYS first, then wire each one. Matches the existing codebase pattern (many stubs already present).
Trade-offs:
- ✅ Compiles throughout; no long broken-build windows
- ❌ Stub-first delays real testability; TDD mandate disfavors this
Recommendation: Option A. The dependency chain is well-defined and single-developer serial implementation is the realistic mode for this C codebase.
4. Effort and Risk Summary
| Req | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Listener before election | M (3–5d) | Medium | Refactor startup order; must remove vtaskmgr/config dependency first |
| 2 — Quorum election | XL (2+ wks) | High | Full algorithm replacement; new wire protocol; FSM extension; concurrent dispatch |
| 3 — Config bootstrap | M (3–5d) | Medium | New function + probe RPC; depends on do_one_config_fetch (wired) |
| 4 — Startup validation | S (1d) | Low | Additive CLI + validation; ASAN one-liner |
| 5 — Majority-commit WAL | L (1–2 wks) | High | committed_lsn cross-cuts persist+repl+ha; repl_broadcast must become real |
| 6 — Persistent channel | XL (2+ wks) | High | New connection lifecycle; full state sync protocol; 3-tier discovery |
| 7 — Client retry | M (3d) | Low | Well-understood retry pattern; additive libxce change |
| 8 — Majority-commit config | M (3d) | Medium | Shares Req 5 infrastructure; mainly wiring push_config_to_standbys |
| 9 — State durability | S (1d) | Low | File write+read; extends ha_init |
| 10 — Observability | S (alongside) | Low | Log statements woven into each change |
Total estimated effort: 6–9 weeks for one engineer implementing with full TDD.
High-risk dependencies:
- Req 2 must complete before Req 3 can be integrated end-to-end.
- Req 5 (
repl_broadcastimplementation) must complete before Req 8 and before Req 6 (WAL streaming over the channel). - Req 4 (vcpus CLI) must complete before Req 1 (vtaskmgr before election).
5. Key Reusable Assets
| Asset | Location | Reuse in |
|---|---|---|
repl_add/remove_standby |
repl.c | Req 6 (channel registration) |
repl_start/complete_catchup |
repl.c | Req 6 (state sync protocol) |
repl_needs_snapshot |
repl.c | Req 6 (snapshot decision) |
repl_on_ack |
repl.c | Req 5 (majority-commit ack handler) |
repl_trim_lsn |
repl.c | Req 5 (WAL trim with committed_lsn) |
connect_ssl_raw() |
netio.c/config_repl.c | Req 3 (probe RPC) |
do_one_config_fetch() |
config_repl.c | Req 3 (deferred fetch) |
xorshift64_next + ha_seed_jitter |
ha.h / ha.c | Req 1, 2 (jitter) |
ha_do_promote / ha_do_demote |
ha.c | Req 2, 6 (role transitions) |
ha_should_redirect / ha_no_primary_known |
ha.c | Req 7 (retry redirect) |
| iomgr epoll + EPOLLIN/EPOLLRDHUP | iomgr (cluster.c) | Req 1, 6 |
| Atomic write+rename pattern | config_repl.c | Req 9 (ha-term.bin) |
| INFO/WARN/ERROR macros | xcelog.h | Req 10 |
6. Constraints for Design Phase
- C gnu17, no C++, no new external dependencies.
- ASAN build (
nix build .#xcecore_debug) must pass after every change. - Static peer membership:
--peerslist is fixed at startup; no dynamic join/leave of controller peers. Quorum denominator is fixed for cluster lifetime. - TDD mandatory: failing test in
src/controller/testsuite/before each implementation change. make testmust pass without a live server at every commit.commit_timeout_msmust be configurable (default 5000ms).- Single-node mode (no
--peers): quorum=1 everywhere; all new majority-commit paths must short-circuit to self-only. - WAL write stall window (operational constraint, not a bug): during standby catch-up after election,
repl_wait_majority_ack()will time out on every write until at least one standby completes catch-up. This is correct CP behavior; operators must be warned in docs.
7. Research Needed (Design Phase)
- vtaskmgr API for pre-election dispatch: confirm that
vtaskmgr_init(vcpus)can be called beforecluster_configis loaded (after removing vcpus from config struct). Verify no hidden config dependency. - iomgr async connect pattern for vote requests: confirm
iomgr_async_vote_request()can be implemented using existing async connect infrastructure without new socket primitives. - capnp schema extension: confirm the capnp code-generation toolchain can add
OP_requestVote,OP_probeConfigTag,OP_walAck,OP_upgradeConfigAck,OP_snapshotTransfer,OP_walCatchupEntrywithout breaking existing decode paths. persist_truncate_wal_above()safety: verify WAL segment file structure supports efficient truncation at arbitrary LSN boundary without full segment rewrite.- libxce client API surface: identify the exact function in src/lib/ that wraps xce_encode()+send+recv+xce_decode() to locate the correct insertion point for retry logic.