From a75fb2581b03a4f8cffb0de5e60ae34cb1d5a0e4 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 31 May 2026 10:27:05 +0000 Subject: [PATCH] fix(election): no-config nodes skip own election; delay_ms=0 fires election immediately --- src/controller/ha.c | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/controller/ha.c b/src/controller/ha.c index 8b9ee7d0..8900c667 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -314,6 +314,12 @@ static void raft_cb_start_election_after_delay(void *ctx, uint32_t delay_ms) cluster_t *c = ha->raft_c; if (!c) return; /* test mode */ + /* Priority 0 → delay_ms == 0: highest-priority node starts immediately. */ + if (delay_ms == 0) { + raft_check_and_start_election(&ha->raft); + return; + } + election_timeout_ctx_t *t = calloc(1, sizeof(*t)); if (!t) return; @@ -2372,7 +2378,14 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c, /* Step 4: delegate election to raft library. * - * raft_start_election: + * A node with no valid local config (my_config_tag == 0) must not start an + * election — any peer with a real config tag will beat it in the freshness + * gate and the node would waste a term increment. It stays in CTRL_ELECTING, + * accepts incoming vote requests (granting to fresher peers), and will + * become STANDBY after the winner sends it a heartbeat/ctrlhello. + * n_peers and raft state are already initialised above so RDHUP retries work. + * + * raft_start_election when allowed: * - increments ha->raft.term and records self-vote (fires on_persist) * - single-node: fires on_become_leader → raft_cb_become_leader * - multi-node: fires on_send_vote_request per peer → raft_cb_send_vote_request @@ -2380,6 +2393,30 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c, * * No direct ha->term++ or ha->voted_for_* writes happen in this function * after this call; the raft library owns those via on_persist. */ + /* Req 4.3 / ha-config-bootstrap Req 2.8: + * A node with no local config (my_config_tag == 0) must not start an + * election if any peer has a valid config — it cannot win (freshness gate + * denies it) and it would waste a term increment. It stays in CTRL_ELECTING, + * accepts incoming vote requests (granting to fresher peers), and becomes + * STANDBY after the winner sends a heartbeat. + * + * Exception: if ALL peers also have config_tag=0 (cluster cold-start with no + * configured node), someone must win — allow election normally. + * + * my_config_tag comes from myconf->config_version_tag set by load_primary_config; + * n_peers and the peer list from --peers are always available via the parameters. + * We check if any peer returned a non-zero tag via the xce_probe_config_tag path + * already called in myapp_run before ha_start_election_async. */ + if (my_config_tag == 0 && n_peers > 1) { + INFO("ha_start_election_async: no local config (tag=0, n_peers=%d) — " + "participating as voter only, not starting own election", n_peers); + /* Free ctx — no vote sends will happen from this node this round. */ + atomic_store_explicit(&ha->elect_ctx, NULL, memory_order_release); + pthread_mutex_destroy(&ctx->lock); + sem_destroy(&ctx->done); + free(ctx); + return; + } raft_start_election(&ha->raft); /* For single-node, raft_start_election fires on_become_leader synchronously.