feat(ha-prevote): route preVoteReply over real transport to the candidate tally (task 5.1)
peer_ctrl_msg_handler gains a CTRL_STANDBY + pre_vote_in_progress branch that decodes an incoming preVoteReply (no term adoption) and feeds it to pre_vote_reply_dispatch with the absolute peer index, paralleling the real-vote reply path. Gives pre_vote_reply_dispatch its production caller (drops the used attribute). STANDBY-no-round still drops; ELECTING/PRIMARY routing unchanged. PreVote now drives rounds to a decision end-to-end over the wire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b765fc4705
commit
6d22f2eb1a
3 changed files with 193 additions and 5 deletions
|
|
@ -2628,6 +2628,11 @@ void handle_request_vote_reply(ha_state_t *ha,
|
|||
static int peer_ctrl_msg_handler(void *ep_raw);
|
||||
static int vote_reply_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx);
|
||||
static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx);
|
||||
/* ha-prevote task 5.1: preVoteReply decode/route; defined alongside the
|
||||
* real-vote decoder, feeds the later-defined pre_vote_reply_dispatch. */
|
||||
static int pre_vote_reply_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx);
|
||||
static void pre_vote_reply_dispatch(ha_state_t *ha, cluster_t *c,
|
||||
int peer_idx, int granted);
|
||||
|
||||
#ifdef XCE_TESTING
|
||||
/*
|
||||
|
|
@ -2860,6 +2865,79 @@ done:
|
|||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* pre_vote_reply_decode_and_dispatch: decode a preVoteReply packet on an
|
||||
* outbound PreVote endpoint and feed it to the PreVote tally (ha-prevote task
|
||||
* 5.1). Mirrors vote_reply_decode_and_dispatch exactly — the PreVote reply
|
||||
* reuses the identical RequestVoteReply payload / union arm — but routes to
|
||||
* pre_vote_reply_dispatch (the raft-library PreVote tally) instead of the
|
||||
* real-vote tally, and deliberately performs NO term adoption (a PreVote reply
|
||||
* never changes our term; Req 1.2). vctx->peer_idx is the ABSOLUTE cluster
|
||||
* index that raft_on_pre_vote_reply expects (set in ha_send_pre_vote_request).
|
||||
* Packet ownership: takes ep->fsm_in.pkt and frees it.
|
||||
*/
|
||||
static int pre_vote_reply_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
||||
{
|
||||
ha_state_t *ha = vctx->ha;
|
||||
cluster_t *c = vctx->c;
|
||||
int idx = vctx->peer_idx; /* ABSOLUTE cluster index */
|
||||
io_packet_t *pkt = ep->fsm_in.pkt;
|
||||
int ok = 0;
|
||||
int vr_granted = 0;
|
||||
|
||||
ep->fsm_in.pkt = NULL; /* take ownership (same pattern as
|
||||
* vote_reply_decode_and_dispatch) */
|
||||
|
||||
if (!pkt || !pkt->data || pkt->sz == 0)
|
||||
goto done;
|
||||
|
||||
{
|
||||
struct capn capn_ctx;
|
||||
struct Request outer_rq;
|
||||
Request_ptr outer_rp;
|
||||
struct RequestReply rep_s;
|
||||
struct RequestVoteReply vr_raw;
|
||||
int capn_ok;
|
||||
|
||||
capn_ok = capn_init_mem(&capn_ctx, (const uint8_t *)pkt->data,
|
||||
(size_t)pkt->sz, 0);
|
||||
if (capn_ok != 0)
|
||||
goto done;
|
||||
|
||||
outer_rp.p = capn_getp(capn_root(&capn_ctx), 0, 1);
|
||||
read_Request(&outer_rq, outer_rp);
|
||||
|
||||
if (outer_rq.body_which != Request_body_replymsg ||
|
||||
!outer_rq.body.replymsg.p.data) {
|
||||
capn_free(&capn_ctx);
|
||||
goto done;
|
||||
}
|
||||
|
||||
read_RequestReply(&rep_s, outer_rq.body.replymsg);
|
||||
|
||||
if (rep_s.data_which != RequestReply_data_requestvotereplymsg) {
|
||||
capn_free(&capn_ctx);
|
||||
goto done;
|
||||
}
|
||||
|
||||
memset(&vr_raw, 0, sizeof(vr_raw));
|
||||
read_RequestVoteReply(&vr_raw, rep_s.data.requestvotereplymsg);
|
||||
|
||||
capn_free(&capn_ctx);
|
||||
|
||||
vr_granted = vr_raw.voteGranted ? 1 : 0;
|
||||
ok = 1;
|
||||
}
|
||||
|
||||
done:
|
||||
if (pkt) { free_packet(pkt); free(pkt); }
|
||||
|
||||
/* A malformed / wrong-body reply counts as a non-grant (best-effort
|
||||
* delivery, per design Event Contract). No term adoption either way. */
|
||||
pre_vote_reply_dispatch(ha, c, idx, ok ? vr_granted : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_ack_decode_and_dispatch: decode an inbound packet on a promoted vote
|
||||
* endpoint while role == CTRL_PRIMARY.
|
||||
|
|
@ -3048,7 +3126,15 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
|||
* Routes by ha->role at dispatch time — no explicit handler reassignment needed.
|
||||
* CTRL_ELECTING → vote_reply_decode_and_dispatch
|
||||
* CTRL_PRIMARY → repl_ack_decode_and_dispatch
|
||||
* CTRL_STANDBY + PreVote → pre_vote_reply_decode_and_dispatch (task 5.1)
|
||||
* any other role → drop packet (WARN)
|
||||
*
|
||||
* PreVote note (ha-prevote task 5.1): a preVoteReply arrives on the candidate's
|
||||
* OUTBOUND PreVote endpoint while the candidate's role is still CTRL_STANDBY
|
||||
* (a PreVote round performs no role change — Req 1.1). Without this branch the
|
||||
* reply would hit the default case and be dropped, stranding the round until it
|
||||
* times out. We gate on raft.pre_vote_in_progress so a STANDBY node with no
|
||||
* active round still drops unexpected traffic.
|
||||
*/
|
||||
static int peer_ctrl_msg_handler(void *ep_raw)
|
||||
{
|
||||
|
|
@ -3058,6 +3144,11 @@ static int peer_ctrl_msg_handler(void *ep_raw)
|
|||
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
|
||||
/* PreVote replies land while CTRL_STANDBY with a round in progress. */
|
||||
if (role == CTRL_STANDBY && ha->raft.pre_vote_in_progress)
|
||||
return pre_vote_reply_decode_and_dispatch(ep, vctx);
|
||||
|
||||
switch (role) {
|
||||
case CTRL_ELECTING:
|
||||
return vote_reply_decode_and_dispatch(ep, vctx);
|
||||
|
|
@ -3275,6 +3366,21 @@ int peer_ctrl_msg_handler_for_test(endpoint_t *ep, io_packet_t *pkt)
|
|||
return peer_ctrl_msg_handler(ep);
|
||||
}
|
||||
|
||||
/*
|
||||
* pre_vote_reply_msg_handler_for_test: drive a preVoteReply through the REAL
|
||||
* decode/route path (peer_ctrl_msg_handler) while the candidate is CTRL_STANDBY
|
||||
* with a PreVote round in progress — the exact production condition under which
|
||||
* a PreVote reply arrives. Unlike vote_reply_msg_handler_for_test it does NOT
|
||||
* force the role: it proves the STANDBY+pre_vote_in_progress branch added in
|
||||
* task 5.1 actually routes the reply to the PreVote tally (instead of dropping
|
||||
* it). Caller owns ep->data (vote_ep_ctx_t); pkt ownership is transferred.
|
||||
*/
|
||||
int pre_vote_reply_msg_handler_for_test(endpoint_t *ep, io_packet_t *pkt)
|
||||
{
|
||||
ep->fsm_in.pkt = pkt;
|
||||
return peer_ctrl_msg_handler(ep);
|
||||
}
|
||||
|
||||
/*
|
||||
* vote_ep_ctx_set_cluster_for_test: set the cluster_t pointer inside the
|
||||
* vote_ep_ctx_t stored at ep->data. Tests that need the promoted-endpoint
|
||||
|
|
@ -3700,11 +3806,10 @@ void ha_start_prevote_async(ha_state_t *ha, cluster_t *c)
|
|||
* Mirrors vote_reply_dispatch → raft_on_vote_reply, but the raft library makes
|
||||
* NO role/term change here — the controller acts on the returned status.
|
||||
*
|
||||
* In 4.1 the only caller is the XCE_TESTING reply seam; the production caller
|
||||
* is the preVoteReply transport dispatch wired in task 5.1. __attribute__((used))
|
||||
* keeps the production build clean (no -Wunused-function) until then.
|
||||
* Production caller (task 5.1): pre_vote_reply_decode_and_dispatch, reached from
|
||||
* peer_ctrl_msg_handler when a preVoteReply arrives on the outbound PreVote
|
||||
* endpoint. Also driven by the XCE_TESTING reply seam.
|
||||
*/
|
||||
__attribute__((used))
|
||||
static void pre_vote_reply_dispatch(ha_state_t *ha, cluster_t *c,
|
||||
int peer_idx, int granted)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue