controller-status, ha-commit-engine, ha-heartbeat-committed-propagation, ha-prevote, ha-single-leader-role, ha-wal-replication, libxce-failover, and raft-ha-core-safety each had every numbered sub-task marked [x] but their group-header lines (e.g. "1. Foundation: ...") left unchecked -- a cosmetic gap, confirmed by roadmap.md's own IMPLEMENTED/VERIFIED notes and zero real unchecked sub-tasks in any of the eight files. No task content changed.
7.5 KiB
Implementation Plan: HA WAL Replication
Task Overview
5 requirements mapped to 4 implementation groups + 1 validation group. Bridging repl.c (replicator) and ha.c/config_repl.c (HA connection layer) so WAL entries transit from primary to standbys and are majority-committed before the caller is acknowledged.
-
1. repl_broadcast TCP send
-
1.1 Fill in repl_broadcast send path
- Remove
(void)payload; (void)payload_len;and thepending_ack_count++counter. - For each non-catch-up standby (
s->ep != NULL, !in_catch_up), build a single-entrywal_catchup_entry_twithlsn=hdr->lsn,entry_type=hdr->type,raw_payload=payload,raw_payload_len=payload_len. - Enqueue via
xce_wal_batch_req_raw(s->ep, &entry, 1). - If
xce_wal_batch_req_rawfails, log WARN and continue to next standby. - Pending-ack-count eviction guard (line 217-225) stays; evicted standbys skip the send.
- Compilation succeeds;
/xce-build controllergreen. - Requirements: 2.1, 2.2, 2.3, 2.4
- Boundary: Replicator (repl.c)
- Remove
-
1.2 Verify no standbys → immediate return
- Confirm existing guard: when
r->standbys[]is all-NULL, loop iterates zero times, returns 0. - Add unit test: alloc replicator with zero standbys, call
repl_broadcast, verify returns 0 without crashing or sending. - Requirements: 2.4
- Boundary: Replicator (repl.c)
- Confirm existing guard: when
-
2. HA→replicator standby registration bridge
-
2.1 Register standbys on connect
- In
raft_cb_become_leader(ha.c), after promoting vote eps toha->standby_endpoints: for each promoted ep, callrepl_add_standby(&c->persist.repl, ep, peer_host, peer_port). - In
on_standby_connected(ha.c), after ctrlhello is sent: callrepl_add_standbywith the late-joiner's endpoint, host, and port. - Host/port obtained from
c->myconf->peers_cfg.candidates[i]. - Log DEBUG3 on successful registration.
- Requirements: 1.1
- Boundary: HA connection layer (ha.c, config_repl.c)
- In
-
2.2 Deregister standbys on disconnect
- In
vote_ep_err_handler(ha.c): before or after removing ep fromha->standby_endpoints, callrepl_remove_standby(&c->persist.repl, ep). - In
standby_ep_err_handler_primary(ha.c): callrepl_remove_standbybefore endpoint cleanup. repl_remove_standbyis NULL-safe (returns early if ep NULL).- Requirements: 1.2
- Boundary: HA connection layer (ha.c)
- In
-
2.3 Idempotent re-registration
repl_add_standbyalready handles duplicate ep: it finds the existing slot by ep pointer match, resets fields.- Unit test: call
repl_add_standbytwice with same ep, verify no double-count and no crash. - Requirements: 1.3
- Boundary: Replicator (repl.c)
-
3. Standby WAL ACK reply
-
3.1 Send WalAckReply after WAL entry applied
- In
ha_apply_wal_catchup_entry(ha.c), afterha_apply_single_entrysucceeds for an entry: construct aWalAckReply { term=ha->term, lsn=ent->lsn, success=1 }and send viaxce_replyto the endpoint that delivered the entry. - The endpoint is not directly available in
ha_apply_wal_catchup_entry— it's called fromcluster.c:handle_wal_catchup_entrywhich has the ep. Passepas a new parameter:ha_apply_wal_catchup_entry(ha, p, entry, ep). - Requirements: 3.1, 3.2
- Boundary: Standby dispatch (cluster.c, ha.c)
- In
-
3.2 Skip ACK on apply failure
- In
ha_apply_single_entry(ha.c): return distinct error codes for skip-vs-crash. - In
ha_apply_wal_catchup_entry: ifha_apply_single_entryreturns non-zero, skip the ACK for that entry (already logged as ERROR). - Requirements: 3.3
- Boundary: Standby dispatch (ha.c)
- In
-
3.3 Standby role guard
- The existing
ha_dispatch_gatealready allowsOPCode_walCatchupEntryinCTRL_STANDBY,CTRL_ELECTING, andCTRL_INIT. - Verify with unit test: standby applies WAL entry while in
CTRL_STANDBY,CTRL_ELECTING,CTRL_INIT— all succeed. - Requirements: 3.4
- Boundary: Standby dispatch (cluster.c, ha.c)
- The existing
-
4. Primary ACK handler → advance committed_lsn
-
4.1 Handle incoming WalAckReply on primary
- In
repl_ack_decode_and_dispatch(ha.c:2506), add case forRequestReply_data_walackreplymsg. - Extract
lsnandsuccessfromWalAckReply. Ifsuccess, callrepl_on_ack(&c->persist.repl, ep, lsn). repl_on_ack(repl.c:158) already updatesacked_lsnand signalsack_cond— no changes needed there.- Requirements: 4.3
- Boundary: Primary heartbeat handler (ha.c)
- In
-
4.2 Verify majority-ack advances committed_lsn
persist_wal_sync_appendalready callsrepl_wait_majority_ackand advancescommitted_lsnon success.- Integration test: start 3-node cluster, submit a job, verify
committed_lsn > 0on primary after submit. - Requirements: 4.1, 4.2, 4.4
- Boundary: Persistence (persist.c, repl.c)
-
5. Validation: tests and integration
-
5.1 Unit test: repl_broadcast sends to registered standby
- Alloc replicator, register a mock endpoint via
repl_add_standby. - Call
repl_broadcastwith test header + payload. - Verify
xce_wal_batch_req_rawwas invoked (via test hook pattern). - Verify payload bytes round-trip through encode→decode.
- Requirements: 2.1, 5.2
- Boundary: Test (testsuite/test_ha_wal_repl.c)
- Alloc replicator, register a mock endpoint via
-
5.2 Unit test: ACK round-trip
- Standby side: encode WalAckReply, decode on primary side, call
repl_on_ack. - Verify
acked_lsnupdated,ack_condsignaled,repl_wait_majority_ackunblocks. - Requirements: 4.3, 5.3
- Boundary: Test (testsuite/test_ha_wal_repl.c)
- Standby side: encode WalAckReply, decode on primary side, call
-
5.3 Unit test: registration lifecycle
- Register, verify
nstandbys==1andstandbys[0].ep == ep. - Register duplicate, verify
nstandbys==1(idempotent). - Deregister, verify slot freed (
ep==NULL,nstandbys==0). - Requirements: 1.1, 1.2, 1.3
- Boundary: Test (testsuite/test_ha_wal_repl.c)
- Register, verify
-
5.4 Integration test: 3-node cluster WAL replication
- Deploy via
/xce-deploy-ha; verify primary elected. - Submit job via
jobsub; verify job appears injobliston primary AND on all standbys. - Kill primary; verify promoted standby has the job (failover durability).
- Verify
committed_lsnadvanced on all nodes. - Requirements: 4.1, 5.5
- Boundary: Integration (deploy-ha)
- Deploy via
-
5.5 ASAN and regression
- Full controller unit suite green under ASAN.
- No new ASAN leaks or errors vs baseline.
/xce-build controllergreen.- Requirements: 5.5
- Boundary: Build (meson)
- 45 tests (308 total assertions) pass. ASAN leaks in test_ha_rdhup_integration and test_ha_start_election_async are pre-existing (last touched by election-iomgr-refactor/checkpoint commits, not ha-wal-replication). Zero new leaks introduced by this feature.
Implementation Notes
- 5.4 VERIFIED 2026-07-02: deployed a 3-node cluster to
~/tmp/xce-wal-repl-deploy(real ext4). Submitted a job on primary ctrl-1;ctrlstatuson all 3 nodes showedcommittedLsn=2matching (replication confirmed — note:joblistitself is gated primary-only,E_XCE_STANDBY/10014, by design inha.c'sCTRL_STANDBYdispatch switch, so standby replication is verified via matchingcommittedLsn/appliedLsn, not a direct standbyjoblistread). Killed primary ctrl-1; ctrl-3 was promoted (term 2→4);joblistagainst the surviving pair (masters=all 3, client transparently rotates) showed the job intact;committedLsn=3matched on both surviving nodes post-failover. PASS on all 4 Observable bullets.