From 7f974e766b88c770c7119bc8bd72e0df9bc18c80 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 13 Jun 2026 13:59:42 +0000 Subject: [PATCH] test(ha-single-leader-role): sweep all COMMITTING fixtures to direct-PRIMARY contract (task 5.1) --- .kiro/specs/ha-single-leader-role/tasks.md | 3 +- .../testsuite/test_commit_barrier_rollout.c | 5 +- src/controller/testsuite/test_config_repl.c | 13 ---- .../testsuite/test_ha_connection_reuse.c | 16 ++--- .../testsuite/test_ha_rdhup_integration.c | 52 ++++++++-------- src/controller/testsuite/test_ha_repl_push.c | 44 ++++++------- src/controller/testsuite/test_ha_role.c | 58 +++++------------- .../testsuite/test_ha_role_lifecycle.c | 36 ++++++----- .../testsuite/test_ha_start_election_async.c | 15 +++-- src/controller/testsuite/test_ha_startup.c | 61 +++++++++++-------- src/controller/testsuite/test_ha_vote_gate.c | 4 +- 11 files changed, 138 insertions(+), 169 deletions(-) diff --git a/.kiro/specs/ha-single-leader-role/tasks.md b/.kiro/specs/ha-single-leader-role/tasks.md index ab91af86..bd35ae99 100644 --- a/.kiro/specs/ha-single-leader-role/tasks.md +++ b/.kiro/specs/ha-single-leader-role/tasks.md @@ -53,7 +53,7 @@ - _Requirements: 1.2, 1.3, 1.4_ - [ ] 5. Test-fixture sweep and final symbol removal -- [ ] 5.1 Retarget the remaining COMMITTING-referencing fixtures +- [x] 5.1 Retarget the remaining COMMITTING-referencing fixtures - Update expectations COMMITTING→PRIMARY (census: research.md §3): `test_ha_role.c` (37 refs), `test_ha_rdhup_integration.c` (20), `test_ha_repl_push.c` (14), `test_ha_startup.c` (12), `test_ha_connection_reuse.c` (11), `test_ha_start_election_async.c` (7), `test_ha_vote_gate.c` (6, residual from 4.2); delete the 11 `cfg.committing_timeout_ms = 10000;` initializers in `test_config_repl.c` - Each updated test asserts the new contract (direct PRIMARY, no transitional observations), not stubbed compatibility - Observable completion: full controller suite green; `grep -rln "COMMITTING" src/controller/testsuite/` returns zero files @@ -87,6 +87,7 @@ - **(3.1) Legacy path routes through BARE helpers, not `ha_promote_to_primary`**: `ha_elect_evaluate_and_signal`'s PRIMARY branch calls `ha_register_persist_hooks` + `ha_start_primary_heartbeat` + `ha_arm_own_term_gate` directly (matching the raft path), NOT `ha_promote_to_primary` — because the latter calls `ha_do_promote` (term increment) and the legacy branch historically has NO term store/increment (research §4 finding 2). This is a deliberate deviation from design §283's loose wording ("route through ha_promote_to_primary / ha_register_persist_hooks"); the bare-helper route preserves term semantics and is the correct reading. - **(4.1) No shims — DELETE, don't stub** (design §Transitional-State Deletions / §Design Synthesis: "rewritten or DELETED, not stubbed"). Machinery-subject test cases (convergence checker, committing watchdog, ack-driven promotion, ack counting) were DELETED, not shimmed: test_ha_role.c (5 majority_convergence + 2 committing_timeout_promotes_degraded), test_ha_rdhup_integration.c (rdhup_committing_to_primary_on_ack, rdhup_3node_one_ack_promotes), test_ha_connection_reuse.c (dispatcher_committing_handles_repl_ack, promoted_ep_repl_ack_promotes_to_primary). Kept state-entry cases had their deleted-fn CALLS excised (not shimmed). `test_ha_connection_reuse.c` was overlooked by the spec's §3 census disposition for machinery cases — confirmed in census (line 97) but its ack cases needed DELETE, not UPDATE. - **(4.2) Residual stale COMMITTING comments in ha.c** (~lines 443, 826, 1240): documentary prose on the legacy raft_cb_become_leader/convergence path — NOT live branches. Scrub in 5.2/6.x (they fail the task-6.2 repo-wide grep). After 4.2, ZERO live code branches on `CTRL_PRIMARY_COMMITTING` in ha.c; the enum value persists in ha.h:64 until 5.2. +- **(5.1) Meson 300s timeout is now routinely hit** — barrier tests' commit-deadline waits push controller-suite wall-time over the 300s meson cap under load (`Fail: 0, Timeout: 1`). The AUTHORITATIVE green check is the DIRECT binary run (`src/xce.build/controller/controller_unit_test`, EXIT=0, 167 Tests/0 Failures aggregated). For task 6.2's "suite + ASAN green" gate: run the direct binary OR re-run meson until under-cap; a `Timeout: 1` is a wrapper artifact, NOT a test failure. `test_committing_timeout_default_is_60s` was DELETED in 5.1 (not 5.2) because its body used the uppercase production macro `HA_DEFAULT_COMMITTING_TIMEOUT_MS` (caught by the grep gate); the knob it tested dies in 5.2 regardless. - **(4.2) Test-leak / suite-timeout facts**: the controller test target sets `ASAN_OPTIONS=detect_leaks=0` (controller/meson.build:163), so a pre-existing suite-wide LSan leak (~153KB, present at baseline 678b7f8, this spec leaks slightly LESS) is suppressed under the meson gate — LSan exit=1 only on a RAW direct-binary run, not a regression. The controller suite runs ~268s against a 300s meson timeout — borderline; the final task-6.2 full regression may TIMEOUT under machine load — re-run if so (it is not a failure). - **(4.1) Residual for task 6.2 final grep**: 3 DOCUMENTARY PROSE comments in `test_ha_role_lifecycle.c` (~lines 760/815/825) still mention `ha_repl_ack_received`/`committing_timeout_fn` by name (no live code). The 4.1 gate (`src/controller/*.c|*.h`) excludes testsuite so this passed; the task-6.2 repo-wide grep WILL catch them — scrub these comments in 5.x/6.x. Also a non-tracked editor backup `src/controller/.ha.c.~undo-tree~` holds stale refs (irrelevant; not a build input — do not commit). - **(3.1) Expected-failure set after task 3.1 (8 category-(a), all owned downstream)**: task 4.1 DELETES `test_ha_evaluate_committing.c` (3 fails: test_winning_quorum_enters_committing, test_committing_resets_repl_ack_count, test_election_timeout_fn_fires_evaluate); task 5.1 retargets `test_nonblocking_single_node_election_enters_committing` (test_ha_startup.c), `test_3node_win_calls_both_pushes` (test_ha_repl_push.c), and 3 RDHUP cases (test_ha_rdhup_integration.c: rdhup_full_path_reaches_committing, rdhup_committing_to_primary_on_ack, rdhup_3node_one_ack_promotes). `committing_timeout_fn` now has a benign `-Wunused-function` warning (no arming site left) until task 4.1 deletes it. diff --git a/src/controller/testsuite/test_commit_barrier_rollout.c b/src/controller/testsuite/test_commit_barrier_rollout.c index 4a754638..1105de48 100644 --- a/src/controller/testsuite/test_commit_barrier_rollout.c +++ b/src/controller/testsuite/test_commit_barrier_rollout.c @@ -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, diff --git a/src/controller/testsuite/test_config_repl.c b/src/controller/testsuite/test_config_repl.c index 407685c9..673f597d 100644 --- a/src/controller/testsuite/test_config_repl.c +++ b/src/controller/testsuite/test_config_repl.c @@ -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; diff --git a/src/controller/testsuite/test_ha_connection_reuse.c b/src/controller/testsuite/test_ha_connection_reuse.c index 669b992a..3af988d0 100644 --- a/src/controller/testsuite/test_ha_connection_reuse.c +++ b/src/controller/testsuite/test_ha_connection_reuse.c @@ -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"). diff --git a/src/controller/testsuite/test_ha_rdhup_integration.c b/src/controller/testsuite/test_ha_rdhup_integration.c index 9dba1ef1..0e4d2245 100644 --- a/src/controller/testsuite/test_ha_rdhup_integration.c +++ b/src/controller/testsuite/test_ha_rdhup_integration.c @@ -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); } diff --git a/src/controller/testsuite/test_ha_repl_push.c b/src/controller/testsuite/test_ha_repl_push.c index 22b807c7..9f3ad400 100644 --- a/src/controller/testsuite/test_ha_repl_push.c +++ b/src/controller/testsuite/test_ha_repl_push.c @@ -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); diff --git a/src/controller/testsuite/test_ha_role.c b/src/controller/testsuite/test_ha_role.c index bcf960e6..b1f83955 100644 --- a/src/controller/testsuite/test_ha_role.c +++ b/src/controller/testsuite/test_ha_role.c @@ -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); } diff --git a/src/controller/testsuite/test_ha_role_lifecycle.c b/src/controller/testsuite/test_ha_role_lifecycle.c index a271235d..86246c34 100644 --- a/src/controller/testsuite/test_ha_role_lifecycle.c +++ b/src/controller/testsuite/test_ha_role_lifecycle.c @@ -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 diff --git a/src/controller/testsuite/test_ha_start_election_async.c b/src/controller/testsuite/test_ha_start_election_async.c index 3e8680b6..a085cc64 100644 --- a/src/controller/testsuite/test_ha_start_election_async.c +++ b/src/controller/testsuite/test_ha_start_election_async.c @@ -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); diff --git a/src/controller/testsuite/test_ha_startup.c b/src/controller/testsuite/test_ha_startup.c index 00a676ed..e33b8780 100644 --- a/src/controller/testsuite/test_ha_startup.c +++ b/src/controller/testsuite/test_ha_startup.c @@ -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 */ diff --git a/src/controller/testsuite/test_ha_vote_gate.c b/src/controller/testsuite/test_ha_vote_gate.c index b1a7c9bd..281f78cd 100644 --- a/src/controller/testsuite/test_ha_vote_gate.c +++ b/src/controller/testsuite/test_ha_vote_gate.c @@ -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. */