fix(ha-bootstrap): priority staggering and candidate_id in election

Two Raft correctness fixes:

1. Priority staggering in ha_wait_for_heartbeat_or_timeout():
   Add my_priority × election_priority_step_ms to the timeout so
   ctrl-a (priority=0) times out first and starts election before
   ctrl-b (priority=1) and ctrl-c (priority=2). This gives the
   highest-priority node a head start and prevents simultaneous
   campaigning that causes perpetual split votes.

2. candidate_id in vote RPC:
   Pass the actual node name (myconf->name) as candidate_id instead
   of "". This fixes the voted_for deduplication gate (Req 2.6):
   a peer that already voted for ctrl-a will correctly deny a
   second vote request from ctrl-b in the same term.

   Thread context gets my_name field; am_i_primary_quorum() takes
   my_name parameter; all call sites updated (myapp.c, ha.c RDHUP,
   test_ha_election.c stubs).
This commit is contained in:
Rongsong Shen 2026-05-30 02:48:59 +00:00
parent fcfb82f7c7
commit 80950ffbba
4 changed files with 20 additions and 9 deletions

View file

@ -636,8 +636,11 @@ ctrl_role_t ha_wait_for_heartbeat_or_timeout(ha_state_t *ha)
if (ha->jitter_seed == 0)
ha_seed_jitter(&ha->jitter_seed, ha->my_priority);
/* Compute timeout in milliseconds: election_timeout + random jitter */
/* Compute timeout: election_timeout + priority_step * priority + random jitter.
* Higher-priority (lower index) nodes time out sooner, giving them a head
* start in the election before lower-priority nodes begin campaigning. */
long to_ms = (long)ha->config.election_timeout_ms;
to_ms += (long)ha->my_priority * (long)ha->config.election_priority_step_ms;
if (ha->config.election_jitter_max_ms > 0) {
to_ms += (long)(xorshift64_next(&ha->jitter_seed)
% ((uint64_t)ha->config.election_jitter_max_ms + 1));
@ -1020,6 +1023,7 @@ typedef struct {
uint64_t my_tag; /* config_tag sent in the request */
uint64_t my_lsn; /* WAL LSN sent in the request */
int timeout_ms; /* Connect + I/O timeout */
const char *my_name; /* Candidate node name for voted_for deduplication */
} vote_thread_ctx_t;
/*
@ -1097,7 +1101,7 @@ static void *vote_request_thread(void *arg)
request_vote_req_t vote_req;
memset(&vote_req, 0, sizeof(vote_req));
vote_req.term = vctx->my_term;
vote_req.candidate_id = (char *)""; /* anonymous; peer reads configTag/lsn */
vote_req.candidate_id = (char *)(vctx->my_name ? vctx->my_name : "");
vote_req.config_tag = vctx->my_tag;
vote_req.last_log_lsn = vctx->my_lsn;
@ -1229,7 +1233,8 @@ ctrl_role_t am_i_primary_quorum(ha_state_t *ha,
int n_peers,
xce_candidate_t **peers,
uint64_t my_config_tag,
uint64_t my_lsn)
uint64_t my_lsn,
const char *my_name)
{
/* Req 2.9: single-node self-elects immediately */
if (n_peers == 1) {
@ -1320,6 +1325,7 @@ ctrl_role_t am_i_primary_quorum(ha_state_t *ha,
vctxs[i].my_lsn = my_lsn;
vctxs[i].timeout_ms =
(int)ha->config.probe_connect_timeout_ms;
vctxs[i].my_name = my_name;
pthread_create(&tids[i], NULL, vote_request_thread, &vctxs[i]);
}
@ -1576,7 +1582,8 @@ void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c)
uint64_t my_tag = c->myconf->config_version_tag;
uint64_t my_lsn = atomic_load_explicit(&c->persist.committed_lsn,
memory_order_acquire);
ctrl_role_t new_role = am_i_primary_quorum(ha, np, peer_ptrs, my_tag, my_lsn);
ctrl_role_t new_role = am_i_primary_quorum(ha, np, peer_ptrs, my_tag, my_lsn,
c->myconf ? c->myconf->name : "");
if (new_role == CTRL_PRIMARY)
INFO("HA: RDHUP recovery → CTRL_PRIMARY (Req 6.8)");
else