test(ha-single-leader-role): sweep all COMMITTING fixtures to direct-PRIMARY contract (task 5.1)

This commit is contained in:
Rongsong Shen 2026-06-13 13:59:42 +00:00
parent 71ce41f486
commit 7f974e766b
11 changed files with 138 additions and 169 deletions

View file

@ -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 */