diff --git a/src/controller/myapp.c b/src/controller/myapp.c index 096fd808..7975b607 100644 --- a/src/controller/myapp.c +++ b/src/controller/myapp.c @@ -113,72 +113,74 @@ int myapp_run(myconf_t* myconf) return rc; } - /* Req 1.1: wait for an incoming heartbeat before starting election. - * If a primary already exists it will send heartbeats; receiving one means - * this node should become STANDBY immediately (skip election). - * If no heartbeat arrives within election_timeout_ms + jitter, proceed to - * election — no primary is reachable and this node should compete. */ - int skip_election = 0; - { - ctrl_role_t hb_role = ha_wait_for_heartbeat_or_timeout(&c.ha); - if (hb_role == CTRL_STANDBY) { - /* A primary is already running — skip election, go straight to standby */ - INFO("HA: heartbeat received — existing primary detected, becoming STANDBY"); - skip_election = 1; - } - /* CTRL_ELECTING: no heartbeat received, proceed to election */ + /* Election retry loop: keep running until this node becomes PRIMARY or + * confirms a primary exists (via heartbeat or election loss). + * + * Iteration: + * 1. Wait for heartbeat (election_timeout_ms + jitter). + * - Heartbeat received → existing primary detected → STANDBY, exit loop. + * - Timeout → no primary visible, proceed to election. + * 2. Run election (2 rounds with jitter). + * - Won PRIMARY → exit loop as primary. + * - Lost (split vote or peer won) → loop back to step 1. + * The next heartbeat wait will catch the winner's heartbeat if one exists, + * or fire after election_timeout_ms if it was a split vote. + */ + ctrl_role_t final_role = CTRL_ELECTING; + + /* Build peer pointer array once (stable across iterations) */ + const xce_cluster_config_t *pcfg = &myconf->peers_cfg; + int n_peers = (pcfg->candidates_count > 0) ? pcfg->candidates_count : 1; + xce_candidate_t *peer_ptrs[MAX_HA_CANDIDATES]; + int n_other = 0; + for (int i = 0; i < pcfg->candidates_count; i++) { + if (i != c.ha.my_priority) + peer_ptrs[n_other++] = (xce_candidate_t *)&pcfg->candidates[i]; } - if (!skip_election) { - /* Election: quorum-based role determination (Req 2.1, 2.9). */ - const xce_cluster_config_t *pcfg = &myconf->peers_cfg; - int n_peers = (pcfg->candidates_count > 0) ? pcfg->candidates_count : 1; - - xce_candidate_t *peer_ptrs[MAX_HA_CANDIDATES]; - int n_other = 0; - for (int i = 0; i < pcfg->candidates_count; i++) { - if (i != c.ha.my_priority) - peer_ptrs[n_other++] = (xce_candidate_t *)&pcfg->candidates[i]; + for (;;) { + /* Step 1: wait for heartbeat from an existing primary */ + ctrl_role_t hb_role = ha_wait_for_heartbeat_or_timeout(&c.ha); + if (hb_role == CTRL_STANDBY) { + INFO("HA: heartbeat received — existing primary detected, becoming STANDBY"); + final_role = CTRL_STANDBY; + break; } + /* Step 2: no heartbeat — run election */ uint64_t my_tag = myconf->config_version_tag; 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); if (elected == CTRL_PRIMARY) { - rc = load_primary_config(&c); - if (rc != 0) { - cluster_fini(&c); - myconf_fini(myconf); - return rc; - } - atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY, - memory_order_release); - } else { - if (c.ha.defer_config_fetch) { - const xce_cluster_config_t *p2 = &myconf->peers_cfg; - if (p2->candidates_count > 0) - INFO("HA bootstrap: deferred config fetch from winner %s:%u", - p2->candidates[0].hostname, (unsigned)p2->candidates[0].port); - c.ha.defer_config_fetch = false; - } - if (myconf->config_version_tag == 0) { - rc = fetch_config_from_primary(&c); - if (rc != 0) { - cluster_fini(&c); - myconf_fini(myconf); - return rc; - } - } else { - INFO("Standby has local config (tag=%016llx) — skipping fetch from primary", - (unsigned long long)myconf->config_version_tag); - } + final_role = CTRL_PRIMARY; + break; } + + /* Lost election — loop back; next heartbeat wait will resolve role */ + INFO("HA: election inconclusive — retrying after heartbeat wait"); + } + + /* Post-loop: apply role */ + if (final_role == CTRL_PRIMARY) { + rc = load_primary_config(&c); + if (rc != 0) { + cluster_fini(&c); + myconf_fini(myconf); + return rc; + } + atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY, + memory_order_release); } else { - /* Heartbeat-detected STANDBY: fetch config if not already loaded. */ + /* STANDBY: fetch config from primary if not already loaded */ + if (c.ha.defer_config_fetch) { + if (pcfg->candidates_count > 0) + INFO("HA bootstrap: deferred config fetch from winner %s:%u", + pcfg->candidates[0].hostname, (unsigned)pcfg->candidates[0].port); + c.ha.defer_config_fetch = false; + } if (myconf->config_version_tag == 0) { rc = fetch_config_from_primary(&c); if (rc != 0) { @@ -186,6 +188,9 @@ int myapp_run(myconf_t* myconf) myconf_fini(myconf); return rc; } + } else { + INFO("Standby has local config (tag=%016llx) — skipping fetch from primary", + (unsigned long long)myconf->config_version_tag); } }