From 80950ffbba7868d496548112281d9ff89de3d457 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 30 May 2026 02:48:59 +0000 Subject: [PATCH] fix(ha-bootstrap): priority staggering and candidate_id in election MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/controller/ha.c | 15 +++++++++++---- src/controller/ha.h | 3 ++- src/controller/myapp.c | 3 ++- src/controller/testsuite/test_ha_election.c | 8 +++++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/controller/ha.c b/src/controller/ha.c index 53af8bc4..12641c03 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -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 diff --git a/src/controller/ha.h b/src/controller/ha.h index 78f9a861..a3ccd645 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -347,7 +347,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); /* * handle_request_vote_reply: process an incoming reply to an OP_requestVote RPC. diff --git a/src/controller/myapp.c b/src/controller/myapp.c index 7975b607..9dde2e55 100644 --- a/src/controller/myapp.c +++ b/src/controller/myapp.c @@ -152,7 +152,8 @@ int myapp_run(myconf_t* myconf) uint64_t my_lsn = (uint64_t)atomic_load_explicit(&c.ha.applied_lsn, memory_order_acquire); ctrl_role_t elected = am_i_primary_quorum( - &c.ha, n_peers, (n_other > 0) ? peer_ptrs : NULL, my_tag, my_lsn); + &c.ha, n_peers, (n_other > 0) ? peer_ptrs : NULL, + my_tag, my_lsn, myconf->name); if (elected == CTRL_PRIMARY) { final_role = CTRL_PRIMARY; diff --git a/src/controller/testsuite/test_ha_election.c b/src/controller/testsuite/test_ha_election.c index 226c7dc3..9b9d5954 100644 --- a/src/controller/testsuite/test_ha_election.c +++ b/src/controller/testsuite/test_ha_election.c @@ -108,7 +108,8 @@ ctrl_role_t ha_elect_evaluate_and_signal(ha_state_t *ha, ha_elect_ctx_t *ctx); 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_config_tag, uint64_t my_lsn, + const char *my_name); void handle_request_vote_reply(ha_state_t *ha, ha_elect_ctx_t *ctx, int peer_idx, uint64_t term, @@ -531,7 +532,8 @@ static void test_single_node_self_elects_as_primary(void) 1, /* n_peers = 1 (single node) */ NULL, /* no other peers */ 500ULL, /* my_config_tag */ - 100ULL /* my_lsn */); + 100ULL, /* my_lsn */ + "test-node"); TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY, (int)role); @@ -613,7 +615,7 @@ static void test_split_vote_full_retry_increments_term(void) /* Single-node: am_i_primary_quorum must self-elect and bump the term. * (Multi-node split-vote path is verified in integration tests.) */ - ctrl_role_t role = am_i_primary_quorum(&ha, 1, NULL, 100ULL, 50ULL); + ctrl_role_t role = am_i_primary_quorum(&ha, 1, NULL, 100ULL, 50ULL, "test-node"); TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY, (int)role); TEST_ASSERT_GREATER_THAN_UINT64(initial_term,