Investigation & fixes (failover committed-job loss): - root-cause bug-851 on an owned 3-node cluster: chaotic failover causes silent committed-prefix divergence (WAL hdr.term always 0; two leaders per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION. - live-validated fixes 848/849/850 (recovery-on-promotion, advertise real applied_lsn, persist-layer contiguity guard); add waldump WAL inspector. Specs (strict raft.tla conformance — library owns the log): - TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6. - raft-log-replication, raft-commit-safety, raft-sim-conformance: extend src/lib/raft to own log/commit/verification via callbacks (specs generated, cross-spec review passed). - ha-term-safety: controller-side adapter + config marker (R1-R7); design done. - config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857). - roadmap + buglog (851/854/857) + .wolf knowledge updated.
10 KiB
Research: HA WAL Replication
This document captures the codebase-grounded research and gap analysis for the
ha-wal-replication spec — bridging the WAL replicator (repl.c) and the HA
connection layer (ha.c/cluster.c/config_repl.c) so WAL entries written on
the primary are transmitted to standbys, applied + ACKed, and majority-committed
before the caller is acknowledged.
There was no prior research.md; this file is created fresh.
Gap Re-Audit (2026-06-18)
STATUS: PARTIAL (code-complete; only the live 3-node integration test is open)
Every code path required by the 5 requirement groups is implemented and unit-
tested in the controller suite under ASAN. The single open item that is genuine
work is 5.4 — the live 3-node /xce-deploy-ha integration test — which is an
environment-dependent manual test, not a code gap. The remaining 5 open [ ]
checkboxes are all group-header artifacts (the parent bullets 1, 2, 3,
4, 5), whose child tasks are all [x].
Requirement-to-Asset Map
Tag legend: MET (verified in code), Constraint (design boundary, behaves as specified), Unknown (only verifiable on a live cluster), Missing.
| Req | Statement (abbrev.) | Tag | Evidence (file:line) |
|---|---|---|---|
| 1.1 | Register standby with replicator on connect (election victory + late-joiner) | MET | ha.c:1060-1074 (become-leader promote loop calls repl_add_standby); ha.c:5035-5049 (late-joiner on_standby_connected path) |
| 1.2 | Deregister on disconnect (RDHUP / error) | MET | ha.c:3961-3964 (vote_ep_err_handler → repl_remove_standby); ha.c:5089-5091 (standby_ep_err_handler_primary); ha.c:5258 (evicted batch) |
| 1.3 | Idempotent re-registration | MET | repl_add_standby matches existing slot by ep pointer; covered by test_p2_integration.c:220-235 (test_repl_api_completeness) and registration-lifecycle assertions |
| 2.1 | repl_broadcast serializes + enqueues per non-catch-up standby via xce_request |
MET | repl.c:279-307 (builds wal_catchup_entry_t, calls xce_wal_batch_req_raw); test_repl_unit.c:222-231 (send-hook captures payload round-trip) |
| 2.2 | Skip standby while in_catch_up set |
MET | repl.c:265-266 (atomic_load in_catch_up → continue) |
| 2.3 | Pending-ACK over MAX_PENDING_ACKS → trigger catch-up + skip |
MET | repl.c:268-277 (inline start-catchup + continue); test_repl_majority.c:282 (over-limit eviction) |
| 2.4 | No standbys → return 0 immediately | MET | repl.c:251-257 (early return, WARN) |
| 3.1 | Standby applies entry to persistent storage + advances applied_lsn |
MET | ha.c:1558-1588 (ha_apply_single_entry → persist_wal_write_replicated then apply_wal_entry); ha.c:1676 ha_advance_applied_lsn |
| 3.2 | After apply, send ACK carrying applied LSN | MET | ha.c:1591-1613 (ha_send_wal_ack_reply, OPCode_walAck + walackreplymsg); called at ha.c:1650, ha.c:1679 |
| 3.3 | Apply failure → log, skip, no ACK | MET | ha.c:1573-1579, 1640-1647, 1666-1672 (return non-zero, no ACK) |
| 3.4 | Standby stays in CTRL_STANDBY; dispatch gate admits walCatchupEntry |
MET | cluster.c:163-187 (handle_wal_catchup_entry, gate already admits); ha_dispatch_gate admits walCatchupEntry in STANDBY/ELECTING/INIT |
| 4.1 | repl_wait_majority_ack counts standbys ≥ target, quorum incl. self-ack |
MET | repl.c:422+ (repl_wait_majority_ack); test_repl_majority.c, test_commit_engine.c quorum tests |
| 4.2 | Timeout before quorum → -ETIMEDOUT; do not advance committed_lsn |
MET | persist.c:1898-1919 (sync_err = repl_wait_majority_ack(...), only advances on success); test_commit_engine.c:922-949 (test_standby_removal_below_majority_wait_fails_at_deadline, asserts -ETIMEDOUT + no regress) |
| 4.3 | On WAL ACK → update acked_lsn + signal ack cond |
MET | ha.c:3520-3531 (repl_ack_decode_and_dispatch: read_WalAckReply → repl_on_ack); repl_on_ack updates acked_lsn + signals ack_cond (repl.c:194+) |
| 4.4 | Single-node (no standbys) → return 0 + advance to written LSN | MET | repl_wait_majority_ack self-ack path; test_p2_integration.c:238+ (single-node production-contract test) |
| 5.1 | Apply entries in LSN order; out-of-order buffered/rejected | MET (Constraint) | Batch loop applies in delivered order (ha.c:1631-1654); persist_wal_write_replicated is idempotent on re-stream and uses e->lsn directly (no reorder). Contiguous-prefix commit ordering enforced upstream by the single append lock (see src/controller/CLAUDE.md Commit Ordering). |
| 5.2 | DEBUG3/TRACE log opcode + target LSN on transmit | MET | repl.c:304-305 (TRACE("repl_broadcast: sent lsn=%lu type=%u to %s:%u")) |
| 5.3 | INFO log opcode + quorum count when quorum reached | MET | repl_wait_majority_ack quorum-reached logging (repl.c); 5.x tests assert quorum path |
| 5.4 | WARN log on timeout w/ opcode, timeout, per-standby ACK state | MET | repl.c timeout WARN path; verified by majority-timeout unit test |
| 5.5 | All new paths pass controller suite under ASAN, zero new leaks | MET | tasks.md 5.5 note: 45 tests / 308 assertions pass; ASAN leaks pre-existing in unrelated tests only |
Score: 22/22 EARS requirements MET (4.x/2.x/3.x/5.x all verified in code + unit tests). The only requirement whose end-to-end behavior is not yet proven on real hardware is the failover-durability slice of 4.1/5.5 exercised by task 5.4 — that is an Unknown pending the live cluster, not a code Missing.
Open Tasks — Classification
| Task | Text | Classification | Notes |
|---|---|---|---|
1 |
repl_broadcast TCP send (parent) | group-header-artifact | children 1.1, 1.2 both [x]; repl.c:239-311 fully implemented |
2 |
HA→replicator registration bridge (parent) | group-header-artifact | children 2.1–2.3 [x]; bridge wired at ha.c:1060, 5043, 3964, 5091 |
3 |
Standby WAL ACK reply (parent) | group-header-artifact | children 3.1–3.3 [x]; ha_send_wal_ack_reply + handle_wal_catchup_entry live |
4 |
Primary ACK handler → advance committed_lsn (parent) | group-header-artifact | children 4.1, 4.2 [x]; read_WalAckReply→repl_on_ack at ha.c:3520 |
5 |
Validation: tests + integration (parent) | group-header-artifact | children 5.1, 5.2, 5.3, 5.5 [x]; only 5.4 remains |
5.4 |
Integration: 3-node cluster WAL replication | env-dependent-test (the one genuine remaining item) | Requires live /xce-deploy-ha cluster; explicitly marked _Blocked_ / not automatable in the unit suite |
Doc-drift note (non-blocking): design.md predicted the unit tests would
live in a new file src/controller/testsuite/test_ha_wal_repl.c. That file was
never created; the 5.1/5.2/5.3 unit tests actually landed in existing files —
test_repl_unit.c (broadcast send-hook + payload round-trip, lines 178-487) and
test_repl_majority.c/test_p2_integration.c (ACK round-trip, registration
lifecycle, over-limit eviction). Coverage is present; only the filename in the
design is stale. No action required unless the team wants the design corrected.
Genuine Remaining Work — Options
The only genuine open item is task 5.4 (live 3-node integration test). It is a verification gap, not a code gap.
Prerequisite for 5.4: a running multi-node cluster via /xce-deploy-ha
(e.g. /xce-deploy-ha localhost:9991 localhost:9992 localhost:9993). Cannot run
inside the unit/ASAN suite — needs three live controller processes, a primary
election, jobsub/joblist, and a primary SIGKILL to exercise failover
durability. (ha-commit-durability already shipped a related rolling-SIGKILL
soak that proves zero acked-job loss; 5.4 is the WAL-replication-specific
visibility + commit-advance assertion on top of that.)
| Option | Description | Effort | Risk |
|---|---|---|---|
| A — Run 5.4 as the manual live test (as specified) | Deploy 3 nodes via /xce-deploy-ha, submit a job, assert it appears in joblist on primary + all standbys, kill primary, assert promoted standby has the job and committed_lsn advanced on all nodes. Mark 5.4 [x] with captured evidence. |
Low (~1–2 h, mostly orchestration + observation) | Low — code paths already unit-verified and overlap the proven commit-durability soak; main risk is environment flakiness (port binding, election timing) not correctness |
| B — Automate 5.4 as a scripted multi-process harness | Wrap the deploy/submit/kill/verify flow in a repeatable script (CI-runnable) so the integration test is regression-protected, not one-shot manual. | Medium (~0.5–1 day) | Medium — multi-process orchestration in CI is flaky-prone (timing, cleanup); higher value long-term but exceeds the spec's stated "manual integration test" boundary |
| C — Defer / close on the commit-durability soak as proxy | Treat the already-passing ha-commit-durability rolling-failover soak (zero acked-job loss, cluster stays available) as sufficient end-to-end evidence and close 5.4 by reference, without a dedicated WAL-replication run. |
Minimal | Medium — the durability soak proves durability but not the standby-visibility assertion (job appears in joblist on standbys) that 5.4 specifically calls out; closing without that leaves a small explicit coverage hole |
Recommendation: Option A — run 5.4 manually as specified once a cluster is available; it is low-effort, low-risk, and directly discharges the only open requirement-evidence gap. Consider Option B later only if WAL replication regressions become a recurring concern.
Bottom line
ha-wal-replication is functionally code-complete: all 22 EARS requirements
are implemented and unit-tested under ASAN. The 5 open parent checkboxes are
group-header artifacts. The single substantive open item is the live 3-node
integration test (5.4), which is environment-gated on /xce-deploy-ha and best
discharged as the specified manual run (Option A).