test(ha-single-leader-role): sweep all COMMITTING fixtures to direct-PRIMARY contract (task 5.1)
This commit is contained in:
parent
71ce41f486
commit
7f974e766b
11 changed files with 138 additions and 169 deletions
|
|
@ -74,8 +74,9 @@
|
|||
* green through task 4.2. Fixture copied from test_commit_engine.c
|
||||
* gate_setup (ha_init + legacy standby_endpoints counting, repl
|
||||
* back-pointer NULL; quorum-absent = empty endpoint registry).
|
||||
* CTRL_PRIMARY_COMMITTING is intentionally NOT tested (deleted wholesale
|
||||
* by ha-single-leader-role).
|
||||
* No transitional leader sub-state is tested: the single leader role is
|
||||
* CTRL_PRIMARY (the former transitional state was deleted wholesale by
|
||||
* ha-single-leader-role).
|
||||
*
|
||||
* Task 5.1 adds the config-barrier cases (Req 4.1-4.4) — configAck
|
||||
* per-push arithmetic, push_config_to_standbys barrier-result contract,
|
||||
|
|
|
|||
|
|
@ -958,7 +958,6 @@ static void test_wal_catchup_entry_advances_applied_lsn(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
/* applied_lsn starts at 0 */
|
||||
TEST_ASSERT_EQUAL_UINT64(0,
|
||||
|
|
@ -1000,7 +999,6 @@ static void test_wal_catchup_entry_error_skipped_no_crash(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
atomic_store_explicit(&ha.applied_lsn, 10, memory_order_release);
|
||||
|
||||
|
|
@ -1034,7 +1032,6 @@ static void test_wal_catchup_entry_null_entry_no_crash(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* NULL entry — must not crash, must return error */
|
||||
|
|
@ -1060,7 +1057,6 @@ static void test_wal_catchup_batch_advances_applied_lsn(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* applied_lsn starts at 0 */
|
||||
|
|
@ -1113,7 +1109,6 @@ static void test_wal_catchup_partial_batch(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* Pre-set applied_lsn to 5 to verify monotonic advance */
|
||||
|
|
@ -1162,7 +1157,6 @@ static void test_wal_catchup_batch_error_entry_skipped(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* Good entry at LSN 30, then a NULL pointer (simulates corrupt entry),
|
||||
|
|
@ -1212,7 +1206,6 @@ static void test_wal_catchup_batch_zero_entries_fallback(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* n_entries == 0 → single-entry path from task 2.1 */
|
||||
|
|
@ -1255,7 +1248,6 @@ static void test_wal_catchup_batch_256_entries(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* Allocate 256 sub-entries at consecutive LSNs starting at 1000 */
|
||||
|
|
@ -1318,7 +1310,6 @@ static void test_wal_catchup_batch_applied_lsn_only_after_full_batch(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* Pre-set applied_lsn to a known value so we can distinguish partial
|
||||
|
|
@ -1388,7 +1379,6 @@ static void test_nop_reply_carries_state_report(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* Set a known applied_lsn */
|
||||
|
|
@ -1432,7 +1422,6 @@ static void test_nop_reply_zero_state_fresh_standby(void)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(&ha, &cfg, 0);
|
||||
|
||||
/* applied_lsn stays at 0; config_version_tag stays at 0 → no config yet */
|
||||
|
|
@ -1747,7 +1736,6 @@ static void snap_recv_ha_init(ha_state_t *ha)
|
|||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.heartbeat_timeout_ms = 10000;
|
||||
cfg.election_timeout_ms = 1500;
|
||||
cfg.committing_timeout_ms = 10000;
|
||||
ha_init(ha, &cfg, 0);
|
||||
}
|
||||
|
||||
|
|
@ -2177,7 +2165,6 @@ static void test_ctrlhello_no_repl_messages_enqueued(void)
|
|||
memset(&hcfg, 0, sizeof(hcfg));
|
||||
hcfg.heartbeat_timeout_ms = 10000;
|
||||
hcfg.election_timeout_ms = 1500;
|
||||
hcfg.committing_timeout_ms = 10000;
|
||||
ha_init(&c.ha, &hcfg, 0);
|
||||
|
||||
c.ha.primary_ctrl_fd = -1;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ static ha_config_t g_cfg = {
|
|||
.max_promotion_retries = 5,
|
||||
.election_jitter_max_ms = 0,
|
||||
.election_priority_step_ms = 500,
|
||||
.committing_timeout_ms = 10000,
|
||||
};
|
||||
|
||||
/* ── Stubs for production hooks ─────────────────────────────────────────── */
|
||||
|
|
@ -176,11 +175,11 @@ static void test_dispatcher_electing_calls_vote_reply_hook(void)
|
|||
printf("test_dispatcher_electing_calls_vote_reply_hook: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 2 (dispatcher CTRL_PRIMARY_COMMITTING → repl_ack count) DELETED ─────
|
||||
/* ── Test 2 (dispatcher transitional-leader → repl_ack count) DELETED ─────────
|
||||
* ha-single-leader-role task 4.1. Subject was the deleted ack-counting
|
||||
* promotion machinery (replCatchUpDone incrementing repl_ack_count to drive
|
||||
* COMMITTING → PRIMARY). That path is gone — nothing left to verify. No shim
|
||||
* (design §"Transitional-State Deletions").
|
||||
* promotion machinery (replCatchUpDone incrementing repl_ack_count to drive a
|
||||
* transitional leader state to PRIMARY). That path is gone — nothing left to
|
||||
* verify. No shim (design §"Transitional-State Deletions").
|
||||
*/
|
||||
|
||||
/* ── Test 3: dispatcher drops packet for CTRL_STANDBY ───────────────────── */
|
||||
|
|
@ -346,7 +345,7 @@ static void test_err_handler_removes_promoted_ep_from_standby(void)
|
|||
ha_state_t ha;
|
||||
init_ha_with_standby_eps(&ha);
|
||||
ha.n_peers = 2;
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY,
|
||||
memory_order_release);
|
||||
|
||||
/* Populate standby_endpoints with a heap-allocated mock ep pointer */
|
||||
|
|
@ -368,7 +367,7 @@ static void test_err_handler_removes_promoted_ep_from_standby(void)
|
|||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "err_handler must return 0");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, ha.standby_endpoints->count,
|
||||
"err_handler: ep must be removed from standby_endpoints when role=COMMITTING (Req 4.2)");
|
||||
"err_handler: ep must be removed from standby_endpoints when role=CTRL_PRIMARY (Req 4.2)");
|
||||
|
||||
free(ep->data);
|
||||
free(ep);
|
||||
|
|
@ -481,7 +480,8 @@ static void test_follower_passive_no_outbound_connect(void)
|
|||
/* ── Test 10 (Task 7.3: promoted ep repl_ack promotes to PRIMARY) DELETED ─────
|
||||
* ha-single-leader-role task 4.1. Subject was the deleted ack-counting
|
||||
* promotion machinery (replCatchUpDone incrementing repl_ack_count until the
|
||||
* majority threshold drives COMMITTING → CTRL_PRIMARY via ha_promote_to_primary).
|
||||
* majority threshold promoted a transitional leader state to CTRL_PRIMARY via
|
||||
* ha_promote_to_primary).
|
||||
* The mechanism is gone — promotion is now the heartbeat decision loop +
|
||||
* per-entry majority ack. Nothing left to verify; no shim (design
|
||||
* §"Transitional-State Deletions").
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
* test_ha_rdhup_integration.c — Task 6.4 integration test: RDHUP failover
|
||||
* end-to-end role transition.
|
||||
*
|
||||
* Tests verify the full RDHUP → CTRL_ELECTING → CTRL_PRIMARY_COMMITTING →
|
||||
* CTRL_PRIMARY state-machine chain using XCE_TESTING hook seams. No live
|
||||
* sockets or real cluster_t required.
|
||||
* Tests verify the full RDHUP → CTRL_ELECTING → CTRL_PRIMARY state-machine
|
||||
* chain (the election winner becomes primary directly — single leader role,
|
||||
* ha-single-leader-role) using XCE_TESTING hook seams. No live sockets or
|
||||
* real cluster_t required.
|
||||
*
|
||||
* Requirements: 6.3, 6.5, 9.2, 9.3 (Req numbers from tasks.md 5.2 / 6.4)
|
||||
*
|
||||
|
|
@ -68,7 +69,6 @@ static ha_config_t g_cfg = {
|
|||
.max_promotion_retries = 5,
|
||||
.election_jitter_max_ms = 0,
|
||||
.election_priority_step_ms = 500,
|
||||
.committing_timeout_ms = 10000,
|
||||
};
|
||||
|
||||
/* ── install_stubs / clear_stubs ─────────────────────────────────────────── */
|
||||
|
|
@ -155,13 +155,14 @@ static void make_cluster(cluster_t *c, myconf_t *myconf, const char *my_name)
|
|||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Test 1: RDHUP → ELECTING → COMMITTING (no standbys present → via hook chain)
|
||||
* Test 1: RDHUP → ELECTING → PRIMARY (no standbys present → via hook chain)
|
||||
*
|
||||
* Req 6.3: RDHUP triggers CTRL_ELECTING + async election
|
||||
* Req 9.3: RDHUP election uses ha_start_election_async (not am_i_primary_quorum)
|
||||
* Req 9.2: phase2_done=1 suppresses cluster_init_phase2 on RDHUP re-election
|
||||
* Req 1.1: the election winner becomes CTRL_PRIMARY directly (single leader role)
|
||||
* ═════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_rdhup_full_path_reaches_committing(void)
|
||||
static void test_rdhup_full_path_reaches_primary(void)
|
||||
{
|
||||
install_stubs();
|
||||
|
||||
|
|
@ -193,13 +194,14 @@ static void test_rdhup_full_path_reaches_committing(void)
|
|||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_phase2_calls,
|
||||
"RDHUP re-election must not call cluster_init_phase2 when phase2_done=1 (Req 9.2)");
|
||||
|
||||
/* Role must be CTRL_PRIMARY_COMMITTING (quorum won → committing gate) */
|
||||
/* Role must be CTRL_PRIMARY directly — the election winner is the primary
|
||||
* (single leader role; no transitional sub-state, Req 1.1). */
|
||||
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, role,
|
||||
"RDHUP full path must reach CTRL_PRIMARY_COMMITTING after winning election (Req 6.3)");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, role,
|
||||
"RDHUP full path must reach CTRL_PRIMARY after winning election (Req 6.3, 1.1)");
|
||||
|
||||
ha_fini(&c.ha);
|
||||
printf("test_rdhup_full_path_reaches_committing: PASS (role=%d==CTRL_PRIMARY_COMMITTING)\n",
|
||||
printf("test_rdhup_full_path_reaches_primary: PASS (role=%d==CTRL_PRIMARY)\n",
|
||||
role);
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +213,7 @@ static void test_rdhup_full_path_reaches_committing(void)
|
|||
* ═════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Test 3: RDHUP in single-node cluster → immediate PRIMARY (no COMMITTING)
|
||||
* Test 3: RDHUP in single-node cluster → immediate PRIMARY (single leader role)
|
||||
*
|
||||
* Req 9.1: single-node self-elects without network
|
||||
* n_peers=0 → quorum=1, threshold=0 → acks(0) >= 0 → promotes immediately
|
||||
|
|
@ -243,7 +245,7 @@ static void test_rdhup_single_node_reaches_primary(void)
|
|||
close(sv[1]);
|
||||
|
||||
/* n_peers=0 → ha_elect_evaluate_and_signal single-node shortcircuit lands
|
||||
* CTRL_PRIMARY directly (no transitional COMMITTING, no ack machinery —
|
||||
* CTRL_PRIMARY directly (no transitional leader sub-state, no ack machinery —
|
||||
* ha-single-leader-role). The old replication-ack fallback was excised
|
||||
* (function deleted task 4.1); the PRIMARY assertion stands on its own. */
|
||||
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
|
||||
|
|
@ -258,18 +260,18 @@ static void test_rdhup_single_node_reaches_primary(void)
|
|||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Test 4 (3-node RDHUP — ack-driven promotion) DELETED — ha-single-leader-role
|
||||
* task 4.1. Subject was the deleted ack-counting promotion machinery
|
||||
* (the replication-ack handler drives COMMITTING → PRIMARY on threshold acks);
|
||||
* the mechanism is gone, nothing left to verify. No shim (design §"Transitional-
|
||||
* State Deletions").
|
||||
* (the replication-ack handler promoted a transitional leader state to PRIMARY
|
||||
* on threshold acks); the mechanism is gone, nothing left to verify. No shim
|
||||
* (design §"Transitional-State Deletions").
|
||||
* ═════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Test 5: config push and ctrlhello hooks fire during COMMITTING entry
|
||||
* Test 5: config push and ctrlhello hooks fire on promotion to primary
|
||||
*
|
||||
* Req 7.1: push_config_to_standbys called after winning election
|
||||
* Req 7.2: ha_send_ctrlhello_to_all_standbys called after winning election
|
||||
* ═════════════════════════════════════════════════════════════════════════ */
|
||||
static void test_rdhup_committing_fires_replication_hooks(void)
|
||||
static void test_rdhup_promotion_fires_replication_hooks(void)
|
||||
{
|
||||
install_stubs();
|
||||
|
||||
|
|
@ -293,13 +295,13 @@ static void test_rdhup_committing_fires_replication_hooks(void)
|
|||
close(sv[1]);
|
||||
|
||||
TEST_ASSERT_GREATER_THAN_INT_MESSAGE(0, g_push_config_calls,
|
||||
"push_config_to_standbys hook must fire when entering COMMITTING (Req 7.1)");
|
||||
"push_config_to_standbys hook must fire on promotion to primary (Req 7.1)");
|
||||
TEST_ASSERT_GREATER_THAN_INT_MESSAGE(0, g_ctrlhello_calls,
|
||||
"ha_send_ctrlhello hook must fire when entering COMMITTING (Req 7.2)");
|
||||
"ha_send_ctrlhello hook must fire on promotion to primary (Req 7.2)");
|
||||
|
||||
clear_stubs();
|
||||
ha_fini(&c.ha);
|
||||
printf("test_rdhup_committing_fires_replication_hooks: PASS "
|
||||
printf("test_rdhup_promotion_fires_replication_hooks: PASS "
|
||||
"(push=%d, ctrlhello=%d)\n",
|
||||
g_push_config_calls, g_ctrlhello_calls);
|
||||
}
|
||||
|
|
@ -348,8 +350,8 @@ static void test_rdhup_primary_dispatch_gate_opens(void)
|
|||
close(sv[1]);
|
||||
|
||||
/* ha-single-leader-role: ha_elect_evaluate_and_signal lands the winner in
|
||||
* CTRL_PRIMARY directly (no transitional COMMITTING, no ack machinery). The
|
||||
* old replication-ack drive-step was excised (function deleted task 4.1);
|
||||
* CTRL_PRIMARY directly (no transitional leader sub-state, no ack machinery).
|
||||
* The old replication-ack drive-step was excised (function deleted task 4.1);
|
||||
* the dispatch-gate assertion below is the subject of this case. */
|
||||
int role = (int)atomic_load_explicit(&c.ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT((int)CTRL_PRIMARY, role);
|
||||
|
|
@ -375,12 +377,12 @@ static void test_rdhup_primary_dispatch_gate_opens(void)
|
|||
|
||||
void test_ha_rdhup_integration(void)
|
||||
{
|
||||
RUN_TEST(test_rdhup_full_path_reaches_committing);
|
||||
/* test_rdhup_committing_to_primary_on_ack deleted (ha-single-leader-role 4.1:
|
||||
RUN_TEST(test_rdhup_full_path_reaches_primary);
|
||||
/* ack-driven promotion test deleted (ha-single-leader-role 4.1:
|
||||
* ack-driven promotion machinery removed, no shim) */
|
||||
RUN_TEST(test_rdhup_single_node_reaches_primary);
|
||||
/* test_rdhup_3node_one_ack_promotes deleted (ha-single-leader-role 4.1:
|
||||
* ack-counting promotion machinery removed, no shim) */
|
||||
RUN_TEST(test_rdhup_committing_fires_replication_hooks);
|
||||
RUN_TEST(test_rdhup_promotion_fires_replication_hooks);
|
||||
RUN_TEST(test_rdhup_primary_dispatch_gate_opens);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
/*
|
||||
* test_ha_repl_push.c — Unity tests for Task 4.2:
|
||||
* Config and WAL replication push to standbys in the COMMITTING path.
|
||||
* Config and WAL replication push to standbys on election win (the node
|
||||
* becomes CTRL_PRIMARY directly — single leader role, ha-single-leader-role).
|
||||
*
|
||||
* Tests verify:
|
||||
* Req 7.1: When entering CTRL_PRIMARY_COMMITTING, push_config_to_standbys
|
||||
* Req 7.1: When the node becomes CTRL_PRIMARY, push_config_to_standbys
|
||||
* is called (via ha_push_config_hook seam).
|
||||
* Req 7.2: When entering CTRL_PRIMARY_COMMITTING, ha_send_ctrlhello_to_all_standbys
|
||||
* Req 7.2: When the node becomes CTRL_PRIMARY, ha_send_ctrlhello_to_all_standbys
|
||||
* is called to trigger catch-up replication (via ha_send_ctrlhello_hook seam).
|
||||
* Req 7.3: The push is triggered at the point of COMMITTING entry, not before.
|
||||
* Req 7.3: The push is triggered at the point of promotion, not before.
|
||||
* When election is lost (CTRL_STANDBY), no push occurs.
|
||||
*
|
||||
* Design ref: design.md "ha_send_ctrlhello_to_all_standbys" section
|
||||
* Boundary: ha.c (election evaluation + COMMITTING path)
|
||||
* Boundary: ha.c (election evaluation + direct-PRIMARY promotion path)
|
||||
* Requirements: 7.1, 7.2, 7.3
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
|
|
@ -31,7 +32,7 @@
|
|||
/* ── Test-only seam: ha_send_ctrlhello_to_all_standbys call hook ─────────────
|
||||
*
|
||||
* In production, ha_elect_evaluate_and_signal calls
|
||||
* ha_send_ctrlhello_to_all_standbys(c) in the COMMITTING path.
|
||||
* ha_send_ctrlhello_to_all_standbys(c) on the promotion path.
|
||||
* For unit tests, the real function is not linked (cluster.c not in test binary).
|
||||
* The XCE_TESTING seam in ha.c checks ha_send_ctrlhello_hook; if non-NULL, it
|
||||
* calls the hook instead of the real ha_send_ctrlhello_to_all_standbys.
|
||||
|
|
@ -41,7 +42,7 @@ extern int (*ha_send_ctrlhello_hook)(void *c);
|
|||
/* ── Test-only seam: push_config_to_standbys call hook ──────────────────────
|
||||
*
|
||||
* In production, ha_elect_evaluate_and_signal calls push_config_to_standbys(c,...)
|
||||
* in the COMMITTING path. For unit tests, the seam in ha.c checks
|
||||
* on the promotion path. For unit tests, the seam in ha.c checks
|
||||
* ha_push_config_hook; if non-NULL, it calls the hook instead.
|
||||
*/
|
||||
extern int (*ha_push_config_hook)(void *c);
|
||||
|
|
@ -83,7 +84,6 @@ static ha_config_t g_cfg = {
|
|||
.max_promotion_retries = 5,
|
||||
.election_jitter_max_ms = 0,
|
||||
.election_priority_step_ms = 500,
|
||||
.committing_timeout_ms = 10000,
|
||||
};
|
||||
|
||||
/* ── Helper: build a minimal ha_elect_ctx_t ─────────────────────────────── */
|
||||
|
|
@ -136,12 +136,12 @@ static void clear_hooks(void)
|
|||
ha_phase2_hook = NULL;
|
||||
}
|
||||
|
||||
/* ── Test 1: Req 7.2 — COMMITTING path calls ha_send_ctrlhello_to_all_standbys ── */
|
||||
/* ── Test 1: Req 7.2 — promotion calls ha_send_ctrlhello_to_all_standbys ───── */
|
||||
|
||||
static void test_committing_calls_ctrlhello_push(void)
|
||||
static void test_promotion_calls_ctrlhello_push(void)
|
||||
{
|
||||
/* When election is won (CTRL_PRIMARY → COMMITTING), ha_send_ctrlhello_to_all_standbys
|
||||
* must be called exactly once.
|
||||
/* When election is won (node becomes CTRL_PRIMARY directly),
|
||||
* ha_send_ctrlhello_to_all_standbys must be called exactly once.
|
||||
* Single-node wins: n_other=0, votes_granted=1. */
|
||||
reset_counters();
|
||||
|
||||
|
|
@ -156,18 +156,18 @@ static void test_committing_calls_ctrlhello_push(void)
|
|||
ha_elect_evaluate_and_signal(&ha, ctx);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_ctrlhello_call_count,
|
||||
"ha_send_ctrlhello_to_all_standbys must be called once on COMMITTING entry (Req 7.2)");
|
||||
"ha_send_ctrlhello_to_all_standbys must be called once on promotion (Req 7.2)");
|
||||
|
||||
free_ctx(ctx);
|
||||
ha_fini(&ha);
|
||||
clear_hooks();
|
||||
printf("test_committing_calls_ctrlhello_push: PASS (ctrlhello_calls=%d)\n",
|
||||
printf("test_promotion_calls_ctrlhello_push: PASS (ctrlhello_calls=%d)\n",
|
||||
g_ctrlhello_call_count);
|
||||
}
|
||||
|
||||
/* ── Test 2: Req 7.1 — COMMITTING path calls push_config_to_standbys ──────── */
|
||||
/* ── Test 2: Req 7.1 — promotion calls push_config_to_standbys ────────────── */
|
||||
|
||||
static void test_committing_calls_push_config(void)
|
||||
static void test_promotion_calls_push_config(void)
|
||||
{
|
||||
/* When election is won, push_config_to_standbys must be called once. */
|
||||
reset_counters();
|
||||
|
|
@ -183,12 +183,12 @@ static void test_committing_calls_push_config(void)
|
|||
ha_elect_evaluate_and_signal(&ha, ctx);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_push_config_call_count,
|
||||
"push_config_to_standbys must be called once on COMMITTING entry (Req 7.1)");
|
||||
"push_config_to_standbys must be called once on promotion (Req 7.1)");
|
||||
|
||||
free_ctx(ctx);
|
||||
ha_fini(&ha);
|
||||
clear_hooks();
|
||||
printf("test_committing_calls_push_config: PASS (push_config_calls=%d)\n",
|
||||
printf("test_promotion_calls_push_config: PASS (push_config_calls=%d)\n",
|
||||
g_push_config_call_count);
|
||||
}
|
||||
|
||||
|
|
@ -251,8 +251,8 @@ static void test_3node_win_calls_both_pushes(void)
|
|||
|
||||
ctrl_role_t actual_role =
|
||||
(ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, (int)actual_role,
|
||||
"ha->role must be CTRL_PRIMARY_COMMITTING after winning");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, (int)actual_role,
|
||||
"ha->role must be CTRL_PRIMARY directly after winning (single leader role, Req 1.1)");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_ctrlhello_call_count,
|
||||
"ha_send_ctrlhello_to_all_standbys must be called once (3-node win)");
|
||||
|
|
@ -303,8 +303,8 @@ static void test_done_posted_prevents_double_push(void)
|
|||
|
||||
void test_ha_repl_push(void)
|
||||
{
|
||||
RUN_TEST(test_committing_calls_ctrlhello_push);
|
||||
RUN_TEST(test_committing_calls_push_config);
|
||||
RUN_TEST(test_promotion_calls_ctrlhello_push);
|
||||
RUN_TEST(test_promotion_calls_push_config);
|
||||
RUN_TEST(test_standby_result_does_not_push);
|
||||
RUN_TEST(test_3node_win_calls_both_pushes);
|
||||
RUN_TEST(test_done_posted_prevents_double_push);
|
||||
|
|
|
|||
|
|
@ -151,10 +151,10 @@ static void test_demote_sets_standby_role(void)
|
|||
/*
|
||||
* Verify that ha_init zeroes the new fields added for election-iomgr-refactor:
|
||||
* phase2_done, repl_ack_count, n_peers, client_ssl_ctx
|
||||
* and that CTRL_PRIMARY_COMMITTING is a distinct enum value in ctrl_role_t.
|
||||
* Also verifies ha_config_t.committing_timeout_ms default macro exists.
|
||||
* and that the surviving leader role values are distinct in ctrl_role_t.
|
||||
*
|
||||
* Requirements: 5.2, 8.1 (election-iomgr-refactor spec)
|
||||
* Requirements: 5.2, 8.1 (election-iomgr-refactor spec); 1.2 (ha-single-leader-role:
|
||||
* single leader role — no transitional leader state observable)
|
||||
*/
|
||||
static void test_ha_init_zeroes_new_fields(void)
|
||||
{
|
||||
|
|
@ -165,7 +165,6 @@ static void test_ha_init_zeroes_new_fields(void)
|
|||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
|
||||
|
|
@ -181,14 +180,11 @@ static void test_ha_init_zeroes_new_fields(void)
|
|||
/* client_ssl_ctx must be NULL after ha_init */
|
||||
TEST_ASSERT_NULL(ha.client_ssl_ctx);
|
||||
|
||||
/* committing_timeout_ms stored in config */
|
||||
TEST_ASSERT_EQUAL_UINT32(HA_DEFAULT_COMMITTING_TIMEOUT_MS, ha.config.committing_timeout_ms);
|
||||
|
||||
/* CTRL_PRIMARY_COMMITTING must be a distinct enum value */
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY_COMMITTING, (int)CTRL_PRIMARY);
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY_COMMITTING, (int)CTRL_STANDBY);
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY_COMMITTING, (int)CTRL_INIT);
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY_COMMITTING, (int)CTRL_ELECTING);
|
||||
/* The four surviving roles are distinct: the single leader role is
|
||||
* CTRL_PRIMARY (no transitional leader sub-state, Req 1.2). */
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY, (int)CTRL_STANDBY);
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY, (int)CTRL_INIT);
|
||||
TEST_ASSERT_NOT_EQUAL((int)CTRL_PRIMARY, (int)CTRL_ELECTING);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_ha_init_zeroes_new_fields: PASS\n");
|
||||
|
|
@ -214,7 +210,6 @@ static void test_ha_init_zeroes_standby_convergence_fields(void)
|
|||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
|
||||
|
|
@ -259,7 +254,6 @@ static ha_state_t *build_ha_for_convergence_test(void)
|
|||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
};
|
||||
ha_init(&ha, &cfg, 0);
|
||||
return &ha;
|
||||
|
|
@ -669,28 +663,9 @@ static void test_evaluate_repl_action_stream_wal_fresh_system(void)
|
|||
printf("test_evaluate_repl_action_stream_wal_fresh_system: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Task 5.3: committing timeout default value ──────────────────────────────
|
||||
*
|
||||
* Requirements: 6.4 (ha-repl-hardening)
|
||||
*
|
||||
* The committing_timeout_ms CONFIG knob survives until task 5.2 (it is a
|
||||
* different token from the deleted committing-timeout watchdog callback). The
|
||||
* degraded-mode force-promotion cases that drove the watchdog test wrapper
|
||||
* were machinery-subject and were deleted (ha-single-leader-role task 4.1,
|
||||
* design §"Transitional-State Deletions": no shims). Only the config-default
|
||||
* assertion remains.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Test: HA_DEFAULT_COMMITTING_TIMEOUT_MS is 60000 (not 10000).
|
||||
* Requirements: 6.4 (ha-repl-hardening) — "Change committing_timeout_ms default from 10000 to 60000"
|
||||
*/
|
||||
static void test_committing_timeout_default_is_60s(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(60000, HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
"HA_DEFAULT_COMMITTING_TIMEOUT_MS must be 60000 (Req 6.4, Task 5.3)");
|
||||
printf("test_committing_timeout_default_is_60s: PASS\n");
|
||||
}
|
||||
/* The transitional-leader timeout default test was removed: the config knob
|
||||
* and its default macro are deleted by ha-single-leader-role (the wall-clock
|
||||
* force-promotion watchdog it configured is gone — Req 1.5). */
|
||||
|
||||
/* ── Task 3.3 (ha-wal-replication): dispatch gate allows OPCode_walCatchupEntry
|
||||
* in CTRL_STANDBY, CTRL_ELECTING, and CTRL_INIT.
|
||||
|
|
@ -698,8 +673,8 @@ static void test_committing_timeout_default_is_60s(void)
|
|||
* Requirements: ha-wal-replication Req 3.3 (standby role guard).
|
||||
* Verifies that ha_dispatch_gate(ha, OPCode_walCatchupEntry) returns 0
|
||||
* (allow) for the three roles where a standby legitimately receives WAL
|
||||
* catch-up entries from the primary. CTRL_PRIMARY and
|
||||
* CTRL_PRIMARY_COMMITTING must NOT allow it (they never receive WAL pushes). */
|
||||
* catch-up entries from the primary. CTRL_PRIMARY must NOT allow it
|
||||
* (the leader never receives WAL pushes). */
|
||||
static void test_dispatch_gate_allows_wal_catchup_in_standby_roles(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
|
|
@ -709,7 +684,6 @@ static void test_dispatch_gate_allows_wal_catchup_in_standby_roles(void)
|
|||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
|
||||
|
|
@ -771,10 +745,8 @@ void test_ha_role(void)
|
|||
RUN_TEST(test_dispatch_repl_action_send_snapshot);
|
||||
/* Task 5.2 convergence-checker cases deleted (ha-single-leader-role 4.1:
|
||||
* the majority-convergence checker removed — no shims, machinery-subject) */
|
||||
/* Task 5.3: committing timeout config-default knob (committing_timeout_ms
|
||||
* survives until task 5.2; the degraded-mode committing-timeout watchdog
|
||||
* cases were deleted — ha-single-leader-role 4.1, machinery-subject) */
|
||||
RUN_TEST(test_committing_timeout_default_is_60s);
|
||||
/* transitional-leader timeout default test removed (ha-single-leader-role:
|
||||
* config knob + wall-clock watchdog deleted, Req 1.5) */
|
||||
/* Task 3.3 (ha-wal-replication): dispatch gate standby-role guard */
|
||||
RUN_TEST(test_dispatch_gate_allows_wal_catchup_in_standby_roles);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ static void test_noop_classify_is_retain(void)
|
|||
*
|
||||
* Contract (design §Promotion Rewrite, Reqs 1.1, 1.3, 1.4, 1.5, 2.1, 2.2, 2.3):
|
||||
* every road to PRIMARY lands the node DIRECTLY in CTRL_PRIMARY — no
|
||||
* CTRL_PRIMARY_COMMITTING ever observable — registers the persist hooks,
|
||||
* transitional leader sub-state ever observable — registers the persist hooks,
|
||||
* arms NO wall-clock watchdog, and (production, c != NULL) appends a WAL_NOOP
|
||||
* of the current term then sets the commit engine's own-term gate
|
||||
* (commit_set_writable_lsn) to that no-op's LSN so confirmation of any
|
||||
|
|
@ -477,10 +477,10 @@ static void test_noop_classify_is_retain(void)
|
|||
* - Cases 1-3 drive the raft become-leader callback via its existing
|
||||
* XCE_TESTING shim `raft_cb_become_leader_for_test(ha, term)` (ha.c:3121),
|
||||
* which calls `raft_cb_become_leader` with `ha->raft_c = NULL`. This is
|
||||
* THE seam test_ha_evaluate_committing.c / test_ha_connection_reuse.c use.
|
||||
* RED today: that path stores CTRL_PRIMARY_COMMITTING (ha.c:514), so the
|
||||
* direct-PRIMARY / is_primary / no-redirect / hooks-registered assertions
|
||||
* fail. (The `c == NULL` shim also means the watchdog-arm + noop/gate
|
||||
* THE seam test_ha_connection_reuse.c uses. The contract: that path
|
||||
* lands the node directly in CTRL_PRIMARY, so the direct-PRIMARY /
|
||||
* is_primary / no-redirect / hooks-registered assertions hold. (The
|
||||
* `c == NULL` shim also means the noop/gate
|
||||
* blocks — both under `if (c)` — are skipped, so they are NOT observable
|
||||
* through THIS seam; see cases 4-7 for the real-cluster seam.)
|
||||
* - Cases 4-7 drive the SHARED promotion sequence `ha_promote_to_primary(c)`
|
||||
|
|
@ -522,7 +522,6 @@ static ha_config_t lifecycle_ha_cfg(void)
|
|||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
};
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -543,7 +542,7 @@ static int delaymgr_task_count(const delaymgr_t *mgr)
|
|||
|
||||
/* ── Case 1: DIRECT PRIMARY — become-leader lands in CTRL_PRIMARY, not a
|
||||
* transitional state; ha_is_primary() is true immediately on return (Req 1.1,
|
||||
* 1.4). RED today: raft_cb_become_leader stores CTRL_PRIMARY_COMMITTING. */
|
||||
* 1.4). raft_cb_become_leader stores CTRL_PRIMARY directly. */
|
||||
static void test_promotion_lands_directly_in_primary(void)
|
||||
{
|
||||
#ifdef XCE_TESTING
|
||||
|
|
@ -560,8 +559,7 @@ static void test_promotion_lands_directly_in_primary(void)
|
|||
ctrl_role_t role =
|
||||
(ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, (int)role,
|
||||
"winning election must land DIRECTLY in CTRL_PRIMARY (Req 1.1); "
|
||||
"current code stores CTRL_PRIMARY_COMMITTING — RED");
|
||||
"winning election must land DIRECTLY in CTRL_PRIMARY (Req 1.1)");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha_is_primary(&ha),
|
||||
"ha_is_primary() must be true the instant promotion returns (Req 1.4)");
|
||||
|
||||
|
|
@ -573,9 +571,10 @@ static void test_promotion_lands_directly_in_primary(void)
|
|||
}
|
||||
|
||||
/* ── Case 2: NO REDIRECT — ha_should_redirect() returns 0 the instant
|
||||
* promotion returns, before any standby ACK (Req 1.3). RED today: in
|
||||
* CTRL_PRIMARY_COMMITTING ha_should_redirect can return 1 (a live election
|
||||
* winner pointing clients at a primary that "does not exist yet"). */
|
||||
* promotion returns, before any standby ACK (Req 1.3). A live election winner
|
||||
* is CTRL_PRIMARY, so it never redirects clients to a primary that "does not
|
||||
* exist yet" — the redirect-loop window cannot exist once the single leader
|
||||
* role is the only leader role. */
|
||||
static void test_promotion_no_redirect_from_live_primary(void)
|
||||
{
|
||||
#ifdef XCE_TESTING
|
||||
|
|
@ -594,11 +593,10 @@ static void test_promotion_no_redirect_from_live_primary(void)
|
|||
raft_cb_become_leader_for_test(&ha, /*term=*/1);
|
||||
|
||||
/* Assert: a live primary never redirects (Req 1.3). In CTRL_PRIMARY the
|
||||
* stale hint is ignored; in CTRL_PRIMARY_COMMITTING (today) the hint makes
|
||||
* ha_should_redirect return 1 → RED. */
|
||||
* stale hint is ignored — a node that is itself the primary cannot satisfy
|
||||
* the redirect condition. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, ha_should_redirect(&ha),
|
||||
"a freshly promoted primary must NOT redirect clients (Req 1.3); "
|
||||
"current COMMITTING role + a primary hint yields redirect=1 — RED");
|
||||
"a freshly promoted primary must NOT redirect clients (Req 1.3)");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_promotion_no_redirect_from_live_primary: PASS\n");
|
||||
|
|
@ -1074,9 +1072,9 @@ static void test_standalone_promotion_gate_opens_immediately(void)
|
|||
*
|
||||
* Pin: the legacy real-cluster path produces role/hook/gate/no-watchdog state
|
||||
* identical to the raft road's role and to ha_promote_to_primary's full state.
|
||||
* RED before task 3.1: the legacy branch stored CTRL_PRIMARY_COMMITTING, reset
|
||||
* repl_ack_count, and armed the committing watchdog — diverging from the raft
|
||||
* road on every axis. */
|
||||
* Both roads land directly in CTRL_PRIMARY, arm the own-term gate, and arm no
|
||||
* wall-clock watchdog — symmetric on every axis (the former transitional-state
|
||||
* divergence, with its ack counter and watchdog, is gone). */
|
||||
static void test_legacy_and_raft_promotion_are_symmetric(void)
|
||||
{
|
||||
#ifdef XCE_TESTING
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* shortcircuit delegates to raft_start_election, which fires on_become_leader
|
||||
* synchronously → raft_cb_become_leader (ha-single-leader-role task 2.2),
|
||||
* so the role lands DIRECTLY in CTRL_PRIMARY — there is no transitional
|
||||
* CTRL_PRIMARY_COMMITTING and no committing-timeout watchdog (Req 1.1, 1.5).
|
||||
* leader sub-state and no wall-clock promotion watchdog (Req 1.1, 1.5).
|
||||
* - Function returns in < 5ms (observable via clock_gettime).
|
||||
* - n_peers=1 shortcircuit stores n_peers in ha->n_peers.
|
||||
*
|
||||
|
|
@ -50,7 +50,6 @@ static ha_config_t g_async_cfg = {
|
|||
.max_promotion_retries = 5,
|
||||
.election_jitter_max_ms = 0,
|
||||
.election_priority_step_ms = 500,
|
||||
.committing_timeout_ms = 10000,
|
||||
};
|
||||
|
||||
/* ── Helper: elapsed ms since a timespec ───────────────────────────────── */
|
||||
|
|
@ -116,14 +115,14 @@ static void test_single_node_shortcircuit_evaluates_immediately(void)
|
|||
/* n_peers=1: the shortcircuit fires raft_start_election → on_become_leader
|
||||
* (raft_cb_become_leader) synchronously. ha-single-leader-role (Req 1.1):
|
||||
* a winning node lands DIRECTLY in CTRL_PRIMARY — there is no transitional
|
||||
* CTRL_PRIMARY_COMMITTING. New-leader safety comes from the own-term
|
||||
* leader sub-state. New-leader safety comes from the own-term
|
||||
* WAL_NOOP + writable_lsn gate, not from a fourth role value. */
|
||||
ha_start_election_async(&ha, NULL, 1, NULL, 100ULL, 50ULL, "ctrl-a");
|
||||
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, (int)role,
|
||||
"single-node shortcircuit must enter CTRL_PRIMARY directly (Req 1.1) — "
|
||||
"no transitional CTRL_PRIMARY_COMMITTING");
|
||||
"no transitional leader sub-state");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_single_node_shortcircuit_evaluates_immediately: PASS (role=%d == CTRL_PRIMARY)\n",
|
||||
|
|
@ -140,13 +139,13 @@ static void test_single_node_not_primary_directly(void)
|
|||
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
|
||||
|
||||
/* ha-single-leader-role (Req 1.1): the winning node lands in CTRL_PRIMARY
|
||||
* immediately — the old "must go through COMMITTING first" contract is gone.
|
||||
* Promotion arms NO committing-timeout watchdog (Req 1.5); the own-term gate
|
||||
* (WAL_NOOP + writable_lsn) provides new-leader safety instead. */
|
||||
* immediately — the old "must go through a transitional state first"
|
||||
* contract is gone. Promotion arms NO wall-clock watchdog (Req 1.5); the
|
||||
* own-term gate (WAL_NOOP + writable_lsn) provides new-leader safety. */
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, (int)role,
|
||||
"single-node shortcircuit must land DIRECTLY in CTRL_PRIMARY (Req 1.1) — "
|
||||
"no transitional CTRL_PRIMARY_COMMITTING, no committing watchdog (Req 1.5)");
|
||||
"no transitional leader sub-state, no wall-clock watchdog (Req 1.5)");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_single_node_not_primary_directly: PASS (role=%d == CTRL_PRIMARY)\n", (int)role);
|
||||
|
|
|
|||
|
|
@ -546,34 +546,43 @@ static void test_dispatch_gate_init_allows_heartbeat(void)
|
|||
printf("test_dispatch_gate_init_allows_heartbeat: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Task 3.1: dispatch gate CTRL_PRIMARY_COMMITTING ─────────────────────── */
|
||||
/* ── Task 3.1: dispatch gate — primary without write quorum (single leader
|
||||
* role; the former transitional state is gone, ha-single-leader-role Req 1.2).
|
||||
* A multi-node primary with no connected standbys lacks write quorum
|
||||
* (n_peers=2, standby_endpoints empty → ha_has_write_quorum false), so it
|
||||
* applies the universal commit barrier: job ops are not-ready, replication ops
|
||||
* that build majority are permitted. */
|
||||
|
||||
static void test_dispatch_gate_committing_denies_job_ops(void)
|
||||
static void test_dispatch_gate_primary_no_quorum_denies_job_ops(void)
|
||||
{
|
||||
/* Req 5.1, 8.1: When role is CTRL_PRIMARY_COMMITTING, primary-only
|
||||
* job operations must be denied with XCE_ERR_NOT_READY.
|
||||
/* Req 5.1, 8.1 + commit barrier: a primary lacking write quorum denies
|
||||
* primary-only job operations with XCE_ERR_NOT_READY.
|
||||
* OPCode_jobsub is a representative job-submission opcode. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY,
|
||||
memory_order_release);
|
||||
/* Multi-node cluster with zero connected standbys → no write quorum. */
|
||||
ha.n_peers = 2;
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_jobsub);
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_committing_denies_job_ops: PASS\n");
|
||||
printf("test_dispatch_gate_primary_no_quorum_denies_job_ops: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_committing_allows_repl_ops(void)
|
||||
static void test_dispatch_gate_primary_no_quorum_allows_repl_ops(void)
|
||||
{
|
||||
/* Req 5.2, 8.1: When role is CTRL_PRIMARY_COMMITTING, replication
|
||||
* operations must be permitted (return 0).
|
||||
* OPCode_replCatchUpDone is the key signal that ends the COMMITTING phase. */
|
||||
/* Req 5.2, 8.1: a primary lacking write quorum still permits replication
|
||||
* operations (return 0) — they are how the cluster rebuilds majority.
|
||||
* OPCode_replCatchUpDone is the catch-up completion signal. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY,
|
||||
memory_order_release);
|
||||
/* Multi-node cluster with zero connected standbys → no write quorum. */
|
||||
ha.n_peers = 2;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_replCatchUpDone));
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_heartbeat));
|
||||
|
|
@ -586,7 +595,7 @@ static void test_dispatch_gate_committing_allows_repl_ops(void)
|
|||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_replAck));
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_committing_allows_repl_ops: PASS\n");
|
||||
printf("test_dispatch_gate_primary_no_quorum_allows_repl_ops: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Task 1.1 (ha-heartbeat-liveness): ha_init() zero-initializes last_activity ─
|
||||
|
|
@ -615,8 +624,8 @@ static void test_ha_init_last_activity_is_zero(void)
|
|||
* These tests verify the call-sequence contract by checking that:
|
||||
* 1. ha_start_election_async with n_peers=1 sets ha->n_peers = 1 (single-node
|
||||
* path — the same path myapp_run takes for a single-node cluster).
|
||||
* 2. The role after ha_start_election_async(n_peers=1) is CTRL_PRIMARY_COMMITTING
|
||||
* (not CTRL_PRIMARY) — matching what myapp_run would produce in the new design.
|
||||
* 2. The role after ha_start_election_async(n_peers=1) is CTRL_PRIMARY
|
||||
* directly (single leader role) — matching what myapp_run would produce.
|
||||
* 3. ha_start_election_async completes in < 10ms — confirming myapp_run does not
|
||||
* block before entering cluster_run.
|
||||
*
|
||||
|
|
@ -634,7 +643,6 @@ static ha_config_t g_startup_cfg = {
|
|||
.max_promotion_retries = 5,
|
||||
.election_jitter_max_ms = 0,
|
||||
.election_priority_step_ms = 500,
|
||||
.committing_timeout_ms = 10000,
|
||||
};
|
||||
|
||||
static void test_nonblocking_single_node_election_sets_n_peers(void)
|
||||
|
|
@ -656,12 +664,13 @@ static void test_nonblocking_single_node_election_sets_n_peers(void)
|
|||
printf("test_nonblocking_single_node_election_sets_n_peers: PASS\n");
|
||||
}
|
||||
|
||||
static void test_nonblocking_single_node_election_enters_committing(void)
|
||||
static void test_nonblocking_single_node_election_enters_primary_directly(void)
|
||||
{
|
||||
/* Req 9.3, 8.1: After ha_start_election_async(n_peers=1) the role must be
|
||||
* CTRL_PRIMARY_COMMITTING, NOT CTRL_PRIMARY.
|
||||
/* Req 1.1, 9.3, 8.1: After ha_start_election_async(n_peers=1) the role must
|
||||
* be CTRL_PRIMARY directly — the election winner is the primary, with no
|
||||
* transitional leader sub-state (ha-single-leader-role, single leader role).
|
||||
* This validates that the new myapp_run path (without blocking for(;;) loop)
|
||||
* correctly transitions through the COMMITTING state. */
|
||||
* promotes straight to the primary role. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_startup_cfg, 0);
|
||||
ha.phase2_done = 0;
|
||||
|
|
@ -671,11 +680,11 @@ static void test_nonblocking_single_node_election_enters_committing(void)
|
|||
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
|
||||
&ha.role, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, (int)role,
|
||||
"myapp_run path: single-node must enter CTRL_PRIMARY_COMMITTING after election");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY, (int)role,
|
||||
"myapp_run path: single-node must enter CTRL_PRIMARY directly after election (Req 1.1)");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_nonblocking_single_node_election_enters_committing: PASS (role=%d)\n",
|
||||
printf("test_nonblocking_single_node_election_enters_primary_directly: PASS (role=%d)\n",
|
||||
(int)role);
|
||||
}
|
||||
|
||||
|
|
@ -810,13 +819,13 @@ void test_ha_startup(void)
|
|||
/* Req 1.1 — dispatch gate allows heartbeat in CTRL_INIT */
|
||||
RUN_TEST(test_dispatch_gate_init_allows_heartbeat);
|
||||
|
||||
/* Task 3.1 — dispatch gate: CTRL_PRIMARY_COMMITTING */
|
||||
RUN_TEST(test_dispatch_gate_committing_denies_job_ops);
|
||||
RUN_TEST(test_dispatch_gate_committing_allows_repl_ops);
|
||||
/* Task 3.1 — dispatch gate: primary without write quorum (single leader role) */
|
||||
RUN_TEST(test_dispatch_gate_primary_no_quorum_denies_job_ops);
|
||||
RUN_TEST(test_dispatch_gate_primary_no_quorum_allows_repl_ops);
|
||||
|
||||
/* Task 5.1 — myapp_run non-blocking startup sequence (Req 1.1, 9.3, 9.4) */
|
||||
RUN_TEST(test_nonblocking_single_node_election_sets_n_peers);
|
||||
RUN_TEST(test_nonblocking_single_node_election_enters_committing);
|
||||
RUN_TEST(test_nonblocking_single_node_election_enters_primary_directly);
|
||||
RUN_TEST(test_nonblocking_election_returns_in_under_10ms);
|
||||
|
||||
/* Task 1.1 (ha-heartbeat-liveness) — ha_init zero-initializes last_activity */
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ static void test_primary_role_denies_vote_request(void)
|
|||
|
||||
static void test_primary_role_denies_fresher_same_term_challenger(void)
|
||||
{
|
||||
/* The vote-deny contract that a transitional COMMITTING role used to carry
|
||||
* now lives entirely on CTRL_PRIMARY (single leader role). An elected
|
||||
/* The vote-deny contract lives entirely on CTRL_PRIMARY (single leader
|
||||
* role — there is no transitional leader sub-state). An elected
|
||||
* primary must deny a same-term challenger even when the challenger looks
|
||||
* FRESHER (higher config_tag / LSN): the role gate fires before any
|
||||
* freshness evaluation, protecting the live primary from displacement. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue