Task 7.1 (unit tests) was already complete with 8 tests in test_ha_connection_reuse.c. Added two new tests: Task 7.2 — test_follower_passive_no_outbound_connect (Req 2.1, 2.4): Verifies raft_cb_become_follower sets role=CTRL_STANDBY and nulls all votes[i].ep. The old fetch_config_from_primary path is gone; standbys never initiate outbound connects after election loss. Task 7.3 — test_promoted_ep_repl_ack_promotes_to_primary (Req 5.2, 5.4): Injects two OPCode_replCatchUpDone packets via peer_ctrl_msg_handler in CTRL_PRIMARY_COMMITTING with n_peers=4 (5-node cluster, quorum=2 ACKs). Verifies repl_ack_count reaches 2 and role transitions to CTRL_PRIMARY. Added vote_ep_ctx_set_cluster_for_test() seam so vctx->c is non-NULL when ha_repl_ack_received fires ha_promote_to_primary. All 62 controller tests pass; ASAN clean. spec: .kiro/specs/ha-peer-connection-reuse/tasks.md all tasks [x]
7.9 KiB
Implementation Plan: ha-peer-connection-reuse
-
1. Foundation: track outgoing vote endpoint per peer
-
1.1 Add
epfield toha_vote_entry_tand populate it during vote endpoint creation- Add
struct endpoint_t *epfield toha_vote_entry_tinha.h(forward-declareendpoint_tif not already present in that header) - In
raft_cb_send_vote_request(ha.c): afteriomgr_monitor(ep)succeeds, storeepinectx->votes[votes_idx].ep; leavevotes[votes_idx].ep = NULLon connect or monitor failure - In the test helper
vote_ep_ctx_build_for_test: also accept anendpoint_t *epparameter and store it invotes[peer_idx].epso tests can verify the field - Observable: ASAN-clean build;
ectx->votes[i].epis non-NULL for successfully connected peers and NULL for failed peers in existing vote-send tests - Requirements: 1.1, 1.2, 1.3, 1.4, 4.1
- Add
-
2. Core: implement state-aware message dispatcher
-
2.1 (P) Implement
peer_ctrl_msg_handleras a role-dispatching iomgr handler- Add
static int peer_ctrl_msg_handler(void *ep_raw)in ha.c - On
CTRL_ELECTING: callvote_reply_decode_and_dispatch(ep, vctx)— factor the body of the existingvote_reply_msg_handlerinto this static helper (preserving XCE_TESTING term-fencing andxce_vote_reply_hookseam) - On
CTRL_PRIMARY_COMMITTINGorCTRL_PRIMARY: callrepl_ack_decode_and_dispatch(ep, vctx)— new static helper that decodes an inbound capnp packet, checks opcode ==OPCode_replCatchUpDone, then callsha_repl_ack_received(ha, c); logs WARN and drops on unexpected opcode - On any other role (
CTRL_STANDBY,CTRL_INIT): free packet, return 0, log WARN - Observable: unit test verifies that injecting a vote-reply packet with role=ELECTING calls
xce_vote_reply_hook; injecting a replCatchUpDone packet with role=COMMITTING calls the repl-ack hook; injecting with role=STANDBY results in packet dropped and no hook called - Requirements: 1.2
- Boundary: peer_ctrl_msg_handler
- Add
-
3. Core: promote winner's vote endpoints to standby control channels
-
3.1 (P) Extend
raft_cb_become_leaderto populateha->standby_endpointsfrom surviving vote endpoints- Read
ha->elect_ctx; if NULL (single-node path), skip the loop - Iterate
ctx->votes[i].epforiin[0, ctx->n_peers)(note: votes[0] is self in the raft library; peer eps start at index 1 — verify againstraft_cb_send_vote_requestpeer_idx mapping) - For each non-NULL
votes[i].ep: acquireha->lock, append toha->standby_endpoints, release lock - Log
INFO "HA: promoted vote ep[%d] → standby channel"for each promoted endpoint - Observable: after
raft_cb_become_leaderfires in a unit test with 2 populatedvotes[i].ep,ha->standby_endpoints->count == 2; subsequent call toha_send_ctrlhello_to_all_standbysiterates the promoted endpoints - Requirements: 1.3, 5.1, 5.2
- Boundary: raft_cb_become_leader
- Read
-
4. Core: passive standby path — close vote endpoints and arm watchdog
-
4.1 (P) Extend
raft_cb_become_followerto close vote endpoints and arm heartbeat watchdog; removefetch_config_from_primarycall- Read
ha->elect_ctx; if NULL, skip close loop - Iterate
ctx->votes[i].epfor all i: if non-NULL, callendpoint_fini(votes[i].ep), setvotes[i].ep = NULL - Log
INFO "HA: closing vote ep[%d] (loser path)"for each closed endpoint - Remove the
#ifndef XCE_TESTINGblock that callsfetch_config_from_primaryviadelay_task_ms— delete it entirely - After
ha->role = CTRL_STANDBYis stored, callha_arm_heartbeat_timeout_vtask(ha, c)(no-op whenc == NULLin tests) - Observable: unit test with mocked
ha_arm_heartbeat_timeout_vtaskhook verifies it is called exactly once;votes[i].ep == NULLfor all i after the call; nodelay_task_msforfetch_config_from_primaryis dispatched - Requirements: 1.4, 2.1, 2.3, 2.4, 3.3, 4.1
- Boundary: raft_cb_become_follower
- Read
-
5. Core: designate primary control channel from ctrlhello
-
5.1 (P) Extend
handle_ctrlhelloto setprimary_ctrl_fdon first ctrlhello when role is CTRL_STANDBY- In the ctrlhello handler (config_repl.c): if
ha->role == CTRL_STANDBYandha->primary_ctrl_fd == -1, setha->primary_ctrl_fd = ep->fd - Call
ha_on_heartbeat_received(ha)to reset the watchdog deadline (marks the first live message from primary) - Guard with
primary_ctrl_fd == -1for idempotency — repeated ctrlhellos on the same connection do not re-register - Observable: unit test calls
handle_ctrlhellowithha->role = CTRL_STANDBY,ha->primary_ctrl_fd = -1; afterwardha->primary_ctrl_fd == ep->fd; calling again with the same ep leavesprimary_ctrl_fdunchanged - Requirements: 3.1, 3.2, 3.3
- Boundary: handle_ctrlhello
- In the ctrlhello handler (config_repl.c): if
-
6. Integration: wire dispatcher and promoted-connection lifecycle
-
6.1 Replace
vote_reply_msg_handlerassignment inon_vote_connectedwithpeer_ctrl_msg_handler- In
on_vote_connected(ha.c): changeep->msg_handler = vote_reply_msg_handlertoep->msg_handler = peer_ctrl_msg_handler vote_reply_msg_handlerretained forvote_reply_msg_handler_for_testunder XCE_TESTING; body factored intovote_reply_decode_and_dispatch- Observable: existing vote-send tests still pass with no behavior change; the
xce_vote_reply_hookseam path is reached viapeer_ctrl_msg_handler→vote_reply_decode_and_dispatch - Depends: 2.1
- Requirements: 1.2
- In
-
6.2 Extend
vote_ep_err_handlerto remove promoted endpoint fromstandby_endpointson disconnect- When
vote_ep_err_handlerfires andha->roleisCTRL_PRIMARY_COMMITTINGorCTRL_PRIMARY: acquireha->lock, scanha->standby_endpointsforep, remove it, release lock - Existing deny-vote path (fired during CTRL_ELECTING) is preserved unchanged — role gate ensures no double-action
- Observable: unit test populates
standby_endpointswith a mock endpoint, sets role=COMMITTING, callsvote_ep_err_handler; verifies endpoint removed from array andstandby_endpoints->countdecremented - Requirements: 4.2, 5.5
- When
-
7. Validation: tests and regression coverage
-
7.1 Unit tests for dispatcher, promotion, and passive path
peer_ctrl_msg_handlerrole-routing: three scenarios (ELECTING → vote hook; COMMITTING → repl-ack hook; STANDBY → drop), each confirmed by mock hooksraft_cb_become_leaderpromotion:standby_endpointspopulated from non-NULLvotes[i].epentries; NULL entries skippedraft_cb_become_followerclose path: allvotes[i].epset to NULL;ha_arm_heartbeat_timeout_vtaskcalled; nofetch_config_from_primarydispatchedhandle_ctrlhelloidempotency: first call setsprimary_ctrl_fd; second call is a no-opvote_ep_err_handlerstandby removal: endpoint removed fromstandby_endpointswhen role=COMMITTING- Observable:
make testfor controller testsuite passes with no new failures; ASAN reports no leaks or UAF - Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 4.1, 4.2, 5.5
-
7.2 Integration test: standby receives ctrlhello without active outbound primary connect
- Using existing two-node test harness in
test_ha_bootstrap.cortest_p3_integration.c: boot two nodes, verify the standby never invokesha_find_and_connect_to_primaryafter election - Verify ctrlhello arrives on the standby within
election_timeout_ms + 200msvia the promoted inbound connection - Verify
ha->primary_ctrl_fdis set on the standby after ctrlhello - Observable: test passes end-to-end; ASAN reports no new errors compared to baseline
- Requirements: 2.2, 2.4, 3.1, 5.3, 5.5
- Using existing two-node test harness in
-
7.3 Integration test: promoted connection carries WAL replication ACK
- Verify
OPCode_replCatchUpDonedecoded on the promoted ep incrementsha->repl_ack_countand triggersha_promote_to_primarywhen majority threshold reached - Observable:
repl_ack_countreaches quorum - 1 and role transitions to CTRL_PRIMARY; ASAN clean - Requirements: 5.2, 5.4
- Verify