feat(ha-repl-hardening): majority-convergence → CTRL_PRIMARY transition (task 5.2)

- Add ha_check_majority_convergence() in ha.c: counts standby_converged[],
  promotes CTRL_PRIMARY_COMMITTING → CTRL_PRIMARY when all standbys converge
- Threshold: n_peers (all standbys) per spec test cases; single-node satisfies immediately
- Called from repl_ack_decode_and_dispatch() before replication dispatch
- Logs INFO "majority converged (N/M standbys) → CTRL_PRIMARY" (Req 7.4)
- Three unit tests: 1/2 converged → stays COMMITTING; 2/2 → PRIMARY; single-node → immediate
- Fix ha.h comment: clarify double-promotion prevention via vtask context, not CAS
This commit is contained in:
Rongsong Shen 2026-06-05 04:42:53 +00:00
parent d13010e4e0
commit 534bfd68fe
4 changed files with 218 additions and 1 deletions

View file

@ -377,6 +377,138 @@ static void test_process_standby_state_multiple_peers(void)
printf("test_process_standby_state_multiple_peers: PASS\n");
}
/* ── Task 5.2: ha_check_majority_convergence → PRIMARY transition ────────────
*
* After ha_process_standby_state updates standby_converged[], the primary must
* count the number of converged standbys and promote to CTRL_PRIMARY when the
* majority threshold is met and the current role is CTRL_PRIMARY_COMMITTING.
*
* Threshold formula: n_peers (all standbys must converge to satisfy test cases).
* Single-node (n_peers=0): threshold=0 → satisfied immediately.
*
* Requirements: 2.3, 2.4, 6.1, 6.3, 6.5 (ha-repl-hardening)
*/
#ifdef XCE_TESTING
void ha_check_majority_convergence(ha_state_t *ha, cluster_t *c);
#endif
/*
* Test: 3-node cluster (n_peers=2); only 1/2 standbys converged
* → role must remain CTRL_PRIMARY_COMMITTING.
* Requirements: 2.3, 6.3 (ha-repl-hardening)
*/
static void test_majority_convergence_3node_one_standby_stays_committing(void)
{
#ifdef XCE_TESTING
cluster_t c;
memset(&c, 0, sizeof(c));
ha_config_t cfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.max_promotion_retries = 5,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
};
ha_init(&c.ha, &cfg, 0);
c.ha.n_peers = 2; /* 3-node cluster: primary + 2 standbys */
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
memory_order_release);
/* Only standby[0] converged; standby[1] not converged */
c.ha.standby_converged[0] = true;
c.ha.standby_converged[1] = false;
ha_check_majority_convergence(&c.ha, &c);
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, role,
"3-node: 1/2 standbys converged → must remain CTRL_PRIMARY_COMMITTING (Req 2.3)");
ha_fini(&c.ha);
printf("test_majority_convergence_3node_one_standby_stays_committing: PASS\n");
#else
printf("test_majority_convergence_3node_one_standby_stays_committing: SKIP\n");
#endif
}
/*
* Test: 3-node cluster (n_peers=2); both standbys converged (2/2)
* → role must transition to CTRL_PRIMARY.
* Requirements: 2.3, 2.4, 6.3 (ha-repl-hardening)
*/
static void test_majority_convergence_3node_all_standbys_promotes(void)
{
#ifdef XCE_TESTING
cluster_t c;
memset(&c, 0, sizeof(c));
ha_config_t cfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.max_promotion_retries = 5,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
};
ha_init(&c.ha, &cfg, 0);
c.ha.n_peers = 2; /* 3-node cluster */
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
memory_order_release);
/* Both standbys converged */
c.ha.standby_converged[0] = true;
c.ha.standby_converged[1] = true;
ha_check_majority_convergence(&c.ha, &c);
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, role,
"3-node: 2/2 standbys converged → must transition to CTRL_PRIMARY (Req 2.3, 2.4)");
ha_fini(&c.ha);
printf("test_majority_convergence_3node_all_standbys_promotes: PASS\n");
#else
printf("test_majority_convergence_3node_all_standbys_promotes: SKIP\n");
#endif
}
/*
* Test: single-node cluster (n_peers=0)
* → threshold=0, satisfied immediately → must promote to CTRL_PRIMARY.
* Requirements: 6.5, 2.3 (ha-repl-hardening): single-node is always satisfied.
*/
static void test_majority_convergence_single_node_immediate(void)
{
#ifdef XCE_TESTING
cluster_t c;
memset(&c, 0, sizeof(c));
ha_config_t cfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.max_promotion_retries = 5,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
};
ha_init(&c.ha, &cfg, 0);
c.ha.n_peers = 0; /* single-node: no standbys */
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
memory_order_release);
/* No standbys to converge — threshold=0 must be satisfied immediately */
ha_check_majority_convergence(&c.ha, &c);
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, role,
"single-node: n_peers=0 → threshold=0 satisfied → must promote to CTRL_PRIMARY (Req 6.5)");
ha_fini(&c.ha);
printf("test_majority_convergence_single_node_immediate: PASS\n");
#else
printf("test_majority_convergence_single_node_immediate: SKIP\n");
#endif
}
/* ── Task 3.3: ha_dispatch_repl_action_for_standby PUSH_CONFIG wiring ──────
*
* ha_dispatch_repl_action_for_standby reads the standby's stored
@ -666,6 +798,10 @@ void test_ha_role(void)
RUN_TEST(test_dispatch_repl_action_no_push_when_converged);
/* Task 3.5d: ha_dispatch_repl_action_for_standby SEND_SNAPSHOT wiring */
RUN_TEST(test_dispatch_repl_action_send_snapshot);
/* Task 5.2: ha_check_majority_convergence → PRIMARY transition */
RUN_TEST(test_majority_convergence_3node_one_standby_stays_committing);
RUN_TEST(test_majority_convergence_3node_all_standbys_promotes);
RUN_TEST(test_majority_convergence_single_node_immediate);
}
/* test_am_i_primary suite deleted — am_i_primary() removed (election-iomgr-refactor) */