forked from lei.chen/myxce
feat(ha-prevote): controller voter-side PreVote dispatch (task 3.1)
Add OPCode_preVoteRequest case in cluster.c, mirroring the real-vote case: freshness floor + reused eligibility/stickiness gate (raft_on_pre_vote_request), reply over preVoteReply with the voter's UNCHANGED term (no adoption), denial logging with candidate id + reason. Pure insertion; real-vote case untouched. 6 new vote-gate tests assert deny-while-leader-alive, grant-when-lapsed, and the no-mutation/term-unchanged contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9fac681510
commit
6f95e4cdfc
3 changed files with 444 additions and 2 deletions
|
|
@ -437,6 +437,131 @@ static int myreq_handler(vtask_t* task)
|
|||
rc = 0;
|
||||
break;
|
||||
}
|
||||
case OPCode_preVoteRequest: {
|
||||
/* Task 3.1 (ha-prevote): answer an incoming PreVote straw-poll.
|
||||
*
|
||||
* A PreVote asks "would you vote for me in cand_term (= sender's
|
||||
* current term + 1)?" WITHOUT any side effect. This case mirrors the
|
||||
* OPCode_requestVote case above but is strictly NON-MUTATING:
|
||||
* - it NEVER adopts cand_term (PreVote exists precisely to stop a
|
||||
* temporarily-unreachable peer from inflating the cluster term);
|
||||
* - it records NO vote (raft_on_pre_vote_request writes nothing);
|
||||
* - it never persists.
|
||||
* The reply therefore carries the voter's CURRENT term UNCHANGED, on
|
||||
* both grant and deny (Req 2.3, 6.2).
|
||||
*
|
||||
* Decision order (identical gates to the real-vote path, reused — Req
|
||||
* 2.1, 2.2, 3.1, 3.2, 3.3):
|
||||
* 1. freshness floor (ha_vote_self_freshness): deny if candidate is
|
||||
* below max(local, learned-committed) config_tag/LSN;
|
||||
* 2. raft_on_pre_vote_request: stale-term deny + the SHARED
|
||||
* raft_cb_vote_eligible gate (PRIMARY-deny / fresh-heartbeat
|
||||
* stickiness / catch-up deny).
|
||||
* Every denial is logged with the candidate id + reason (Req 7.2). */
|
||||
request_vote_req_t *vrq = request.body.requestvotemsg;
|
||||
uint64_t cand_tag = vrq ? vrq->config_tag : 0;
|
||||
uint64_t cand_lsn = vrq ? vrq->last_log_lsn : 0;
|
||||
uint64_t cand_term = vrq ? vrq->term : 0;
|
||||
uint64_t local_tag = c->myconf->config_version_tag;
|
||||
uint64_t local_lsn = (uint64_t)atomic_load_explicit(
|
||||
&c->ha.applied_lsn, memory_order_acquire);
|
||||
|
||||
/* Effective freshness floor: max of local state and learned-committed
|
||||
* cluster state (election-safety fix #2, reused unchanged). */
|
||||
uint64_t my_tag = 0, my_lsn = 0;
|
||||
ha_vote_self_freshness(&c->ha, local_tag, local_lsn, &my_tag, &my_lsn);
|
||||
|
||||
int freshness_ok = !(cand_tag < my_tag ||
|
||||
(cand_tag == my_tag && cand_lsn < my_lsn));
|
||||
|
||||
/* Map candidate name → integer peer index (same loop as real-vote). */
|
||||
int from_idx = -1;
|
||||
const char *cand_id = vrq ? vrq->candidate_id : "";
|
||||
if (cand_id && cand_id[0] != '\0') {
|
||||
const xce_cluster_config_t *pcfg = &c->myconf->peers_cfg;
|
||||
for (int i = 0; i < pcfg->candidates_count; i++) {
|
||||
if (strcmp(pcfg->candidates[i].name, cand_id) == 0) {
|
||||
from_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PreVote NEVER adopts the candidate term: reply with our CURRENT term,
|
||||
* read once up front so grant and deny paths share it. */
|
||||
uint64_t reply_term = atomic_load_explicit(&c->ha.term,
|
||||
memory_order_acquire);
|
||||
const char *deny_reason = NULL;
|
||||
int granted = 0;
|
||||
|
||||
if (!freshness_ok) {
|
||||
deny_reason = "freshness";
|
||||
} else if (from_idx < 0) {
|
||||
deny_reason = "unknown_candidate";
|
||||
} else {
|
||||
/* raft_on_pre_vote_request is pure: stale-term + eligibility gate,
|
||||
* no term/voted_for/role mutation, no persist. */
|
||||
int r = raft_on_pre_vote_request(&c->ha.raft, from_idx, cand_term);
|
||||
if (r == RAFT_GRANT) {
|
||||
granted = 1;
|
||||
} else {
|
||||
/* Distinguish the denial reason for operator diagnosis
|
||||
* (Req 7.2). raft_on_pre_vote_request denies on (a) stale term
|
||||
* — cand_term < raft.term — or (b) the shared eligibility gate
|
||||
* (raft_cb_vote_eligible): PRIMARY-deny, fresh-heartbeat
|
||||
* stickiness, or catch-up. We re-derive which fired WITHOUT
|
||||
* calling the file-static heartbeat-fresh predicate: when the
|
||||
* role is STANDBY and the node is neither catching up nor at a
|
||||
* stale term, the only remaining eligibility denial is fresh
|
||||
* heartbeat (a stale heartbeat would have passed the gate). */
|
||||
uint64_t my_raft_term = c->ha.raft.term;
|
||||
if (cand_term < my_raft_term) {
|
||||
deny_reason = "stale_term";
|
||||
} else {
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
|
||||
&c->ha.role, memory_order_acquire);
|
||||
if (role == CTRL_PRIMARY)
|
||||
deny_reason = "primary";
|
||||
else if (atomic_load_explicit(&c->ha.in_catch_up,
|
||||
memory_order_acquire))
|
||||
deny_reason = "catch_up";
|
||||
else if (role == CTRL_STANDBY)
|
||||
deny_reason = "fresh_heartbeat";
|
||||
else
|
||||
deny_reason = "eligibility";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!granted) {
|
||||
INFO("HA: PreVote DENIED candidate='%s' term=%" PRIu64
|
||||
" reason=%s (my_term=%" PRIu64 ", floor tag=%" PRIu64
|
||||
" lsn=%" PRIu64 ")",
|
||||
(cand_id && cand_id[0]) ? cand_id : "<unknown>",
|
||||
cand_term, deny_reason, reply_term, my_tag, my_lsn);
|
||||
}
|
||||
|
||||
request_vote_reply_t pv_rep;
|
||||
memset(&pv_rep, 0, sizeof(pv_rep));
|
||||
pv_rep.term = reply_term; /* UNCHANGED — no adoption */
|
||||
pv_rep.vote_granted = granted;
|
||||
pv_rep.peer_config_tag = my_tag;
|
||||
pv_rep.peer_last_log_lsn = my_lsn;
|
||||
|
||||
/* Reply over the dedicated preVoteReply opcode so the candidate's
|
||||
* reply router (task 5.1) feeds it to the PreVote tally, never the
|
||||
* real-vote tally. */
|
||||
reply.opcode = OPCode_preVoteReply;
|
||||
reply.status = 0;
|
||||
reply.data_which = RequestReply_data_requestvotereplymsg;
|
||||
reply.data.requestvotereplymsg = &pv_rep;
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
/* Inbound PreVote connections are vote-only, like real-vote: leave the
|
||||
* connection open; no state push on the reply. */
|
||||
|
||||
rc = 0;
|
||||
break;
|
||||
}
|
||||
case OPCode_replConfig:
|
||||
rc = handle_config_request(c, ep, &request);
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue