fix(election-iomgr-refactor): Raft election, config decode, and RDHUP chain

- ha.c: self-vote in ha_start_election_async (Raft invariant)
- ha.c: heartbeat_timeout_ms for hb_timeout_thread (not election_timeout_ms)
- ha.c: clear voted_for before RDHUP jitter (stale vote from dead primary)
- ha.c: same-term voted_for check in RDHUP jitter
- config_repl.c/h: use xce_decode for upgradeConfig (capnp extraction before capn_free)
- cluster.c: fix upgradeConfig handler direction (standbys apply, primaries skip)
- cluster.c: remove raw_pkt_copy hack; use req directly
- join.c: push config to newly-joined standbys
- typemap.h/request.h/request.c: raw_sha256/raw_bytes fields for decoded upgradeConfig
- test_ha_failover.c: update heartbeat timeout test to use g_fast_cfg
This commit is contained in:
Rongsong Shen 2026-05-31 03:54:30 +00:00
parent 1ded1022fb
commit dbb750ebd3
8 changed files with 198 additions and 102 deletions

View file

@ -1808,6 +1808,16 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
atomic_fetch_add_explicit(&ha->term, 1, memory_order_acq_rel);
uint64_t cur_term = atomic_load_explicit(&ha->term, memory_order_acquire);
/* Record self-vote before sending RequestVote RPCs. This prevents granting
* a vote to a concurrent candidate in the same term (split-brain guard). */
if (my_name) {
pthread_mutex_lock(&ha->lock);
ha->voted_for_term = cur_term;
strncpy(ha->voted_for_name, my_name, sizeof(ha->voted_for_name) - 1);
ha->voted_for_name[sizeof(ha->voted_for_name) - 1] = '\0';
pthread_mutex_unlock(&ha->lock);
}
/* Transition to CTRL_ELECTING */
atomic_store_explicit(&ha->role, (int32_t)CTRL_ELECTING, memory_order_release);
INFO("HA FSM: → CTRL_ELECTING (async, term=%" PRIu64 ", n_peers=%d)",
@ -2217,29 +2227,37 @@ void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c)
if (committed > 0)
persist_truncate_wal_above(&c->persist, committed);
/* Clear voted_for so the jitter check below reflects NEW votes cast during
* the RDHUP election, not the stale startup voted_for (different term). */
/* Clear voted_for: the old primary is dead; any vote from a previous term
* is stale and must not block the new election. */
pthread_mutex_lock(&ha->lock);
ha->voted_for_term = 0;
memset(ha->voted_for_name, 0, sizeof(ha->voted_for_name));
pthread_mutex_unlock(&ha->lock);
/* Priority-based jitter before RDHUP election to avoid split-brain:
* lower-priority nodes (higher priority index) wait longer, giving the
* highest-priority node time to win before others start their elections. */
/* Priority-based jitter before RDHUP election.
* The highest-priority (index 0) node starts immediately; lower-priority
* nodes sleep for (priority * election_timeout_ms). During the sleep they
* may receive a RequestVote from the priority-0 node — if so, they grant
* it (setting voted_for) and then skip their own election below. */
if (ha->my_priority > 0) {
uint32_t step_ms = ha->config.election_priority_step_ms
? ha->config.election_priority_step_ms
: HA_DEFAULT_ELECTION_PRIORITY_STEP_MS;
uint32_t step_ms = ha->config.election_timeout_ms
? ha->config.election_timeout_ms
: HA_DEFAULT_ELECTION_TIMEOUT_MS;
long delay_ns = (long)ha->my_priority * (long)step_ms * 1000000L;
struct timespec jitter = {
.tv_sec = delay_ns / 1000000000L,
.tv_nsec = delay_ns % 1000000000L,
};
nanosleep(&jitter, NULL);
/* If already voted during jitter (from the winner's vote request), skip */
/* Skip our election if we already voted for ANOTHER candidate in the
* current term during the jitter — meaning the priority-0 node sent
* us a RequestVote, we granted it, and should not compete. */
pthread_mutex_lock(&ha->lock);
int already_voted = (ha->voted_for_name[0] != '\0');
uint64_t cur_term = atomic_load_explicit(&ha->term, memory_order_acquire);
const char *myname_ptr = c && c->myconf ? c->myconf->name : "";
int already_voted = (ha->voted_for_name[0] != '\0' &&
ha->voted_for_term == cur_term &&
strcmp(ha->voted_for_name, myname_ptr) != 0);
pthread_mutex_unlock(&ha->lock);
if (already_voted) {
INFO("HA: RDHUP — already voted during jitter, skipping own election");
@ -2300,7 +2318,9 @@ static void *hb_timeout_thread(void *arg)
ha_state_t *ha = t->ha;
while (!atomic_load(&t->disarmed)) {
/* Compute deadline: election_timeout_ms + rand_jitter */
/* Compute deadline: heartbeat_timeout_ms + rand_jitter.
* Must use heartbeat_timeout_ms (not election_timeout_ms) so standbys
* don't falsely trigger failover while the primary is in COMMITTING. */
uint32_t jitter_ms = 0;
if (ha->config.election_jitter_max_ms > 0) {
struct timespec now;
@ -2308,7 +2328,7 @@ static void *hb_timeout_thread(void *arg)
unsigned int seed = (unsigned int)now.tv_nsec;
jitter_ms = (uint32_t)(rand_r(&seed) % ha->config.election_jitter_max_ms);
}
uint32_t total_ms = ha->config.election_timeout_ms + jitter_ms;
uint32_t total_ms = ha->config.heartbeat_timeout_ms + jitter_ms;
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);