5.8 KiB
Design: HA WAL Replication
Overview
Bridge the WAL replicator and HA connection layer so WAL entries written on the primary are transmitted to standbys, ACKed, and committed via majority quorum. The existing repl_broadcast() is a no-send stub; this design fills in the send path and wires the standby registration/ACK lifecycle.
Architecture
persist_wal_sync_append (persist.c)
→ repl_broadcast(payload) ───→ xce_wal_batch_req_raw(standby ep)
→ repl_wait_majority_ack() ──→ blocks until majority acked_lsn >= target
Standby receive (cluster.c):
OPCode_walCatchupEntry → handle_wal_catchup_entry
→ ha_apply_wal_catchup_entry → persist_wal_write_replicated (durable
write; entity apply is now deferred to the commit-driven apply pump,
not immediate — see note below)
→ send WalAckReply(lsn) [helper: ha_send_wal_ack_reply, ha.c]
Primary receive (ha.c):
OPCode_reply + WalAckReply → repl_on_ack → signal ack_cond
Components
1. repl_broadcast send (repl.c)
Fill in the stub at repl.c:193-234. For each non-catch-up registered standby, serialize the WAL entry into a single-entry wal_catchup_entry_t with raw_payload from the caller's buffer, and enqueue via xce_wal_batch_req_raw(ep, &entry, 1).
- The existing stub already iterates
r->standbys[]and has the catch-up/pending_ack_count guards. - Replace the
(void)payload; (void)payload_len;ands->pending_ack_count++with actual transmission. - The
wal_catchup_entry_tis stack-allocated;xce_wal_batch_req_rawcopies the payload via capnp serialization (caller retains ownership).
2. Standby registration bridge (ha.c)
Wire repl_add_standby / repl_remove_standby into the HA connection lifecycle:
- Register on connect: In
raft_cb_become_leader(ha.c:~513-529), when promoting vote eps toha->standby_endpoints, also callrepl_add_standby(&c->persist.repl, ep, host, port). Inon_standby_connected(ha.c), when a late-joiner connects, callrepl_add_standby. - Deregister on disconnect: In
vote_ep_err_handler(ha.c) andstandby_ep_err_handler_primary(ha.c), callrepl_remove_standby(&c->persist.repl, ep).
The replicator accesses c->persist.repl (already initialized in persist_init).
3. Standby ACK reply (cluster.c / ha.c)
After the standby applies a WAL entry (in ha_apply_single_entry or its caller), send a WalAckReply to the primary via xce_reply:
- The
WalAckReplycarriesterm = ha->term,lsn = applied_entry->lsn,success = 1. - Use
RequestReply_data_walackreplymsgunion arm. - Sent from
ha_apply_wal_catchup_entryafter successful application of each entry in the batch.
4. Primary ACK handler (ha.c)
In repl_ack_decode_and_dispatch (ha.c:2506), add handling for RequestReply_data_walackreplymsg:
- Extract the ACKed LSN from
WalAckReply. - Map the sending endpoint to a peer index via
repl_ep_tcomparison. - Call
repl_on_ack(&c->persist.repl, ep, acked_lsn).
5. WAL entry format
Reuse OPCode_walCatchupEntry with single-entry mode (n_entries=0):
| Field | Source |
|---|---|
| lsn | hdr->lsn |
| entry_type | hdr->type |
| raw_payload | payload buffer from caller |
| raw_payload_len | payload_len |
This is backward-compatible with the existing standby-side handler (ha_apply_wal_catchup_entry in ha.c:986) and the raw wire encoder (xce_wal_batch_req_raw in request.c).
Wire Protocol
Primary Standby
│ │
│── OPCode_walCatchupEntry ──────→ │ (single entry, lsn + payload)
│ │ → apply_wal_entry
│ ←── OPCode_reply ──────────── │ (WalAckReply { lsn, success })
│ → repl_on_ack │
│ → signal majority_ack_cond │
File Structure Plan
| File | Action | Responsibility |
|---|---|---|
src/controller/repl.c |
MODIFY | Fill in repl_broadcast() TCP send |
src/controller/ha.c |
MODIFY | Register/deregister standbys with replicator; handle incoming WalAckReply |
src/controller/cluster.c |
MODIFY | Send WalAckReply after WAL entry applied |
src/controller/testsuite/test_ha_wal_repl.c |
CREATE | Unit tests for broadcast send, ACK round-trip, registration lifecycle |
.kiro/specs/ha-wal-replication/tasks.md |
CREATE | Implementation task list |
Testing Strategy
- Unit (broadcast send): Create a mock endpoint, call
repl_broadcastwith a test entry, verifyxce_wal_batch_req_rawis invoked with correct LSN and payload. - Unit (registration): Call
ha_discover_primarypath that adds a standby, verifyr->standbys[i].ep != NULLandrepl_broadcastiterates it. - Unit (ACK round-trip): Encode a WalAckReply, decode it on the primary side, verify
repl_on_ackupdatesacked_lsnand signals the condition. - Integration: Start 3-node cluster, submit a job, verify WAL appears on all standbys, kill primary, verify promoted standby has the job.
- ASAN: All new tests pass under ASAN with zero leaks.
Out of Boundary
- Changes to
repl_wait_majority_ackquorum logic (already correct). - Changes to the HA heartbeat-driven decision loop (WAL streaming for catching-up standbys is already handled by
ha-repl-hardening). - Config replication path (uses
OPCode_replConfigPush, notrepl_broadcast). - WAL entry serialization format changes (reuses
wal_catchup_entry_t).
Boundary Commitments
repl_broadcasttransmits viaxce_wal_batch_req_raw(single-entry mode), NOT a new custom wire format.- Standby registration uses the existing
repl_add_standby/repl_remove_standbyAPI — no new replicator functions. - The standby ACK uses the existing
WalAckReplycapnp struct — no schema changes needed.