feat(election-iomgr-refactor): restructure myapp_run to fully non-blocking async startup — task 5.1

This commit is contained in:
Rongsong Shen 2026-05-30 12:26:56 +00:00
parent 757558bb33
commit 2e5681387a
4 changed files with 256 additions and 134 deletions

View file

@ -41,6 +41,12 @@ int validate_vcpus(int vcpus);
int validate_startup_config(const char *config_path,
char *warn_buf, size_t warn_len);
/* Task 5.1 — ha_start_election_async (implemented in ha.c, task 2.4) */
void ha_start_election_async(ha_state_t *ha, cluster_t *c,
int n_peers, xce_candidate_t **peers,
uint64_t my_config_tag, uint64_t my_lsn,
const char *my_name);
/* ── Dispatch gate (task 3.3) — does NOT exist yet; linker failure expected ──
*
* ha_dispatch_gate: role-based operation filter for the vtaskmgr dispatch loop.
@ -583,6 +589,105 @@ static void test_dispatch_gate_committing_allows_repl_ops(void)
printf("test_dispatch_gate_committing_allows_repl_ops: PASS\n");
}
/* ── Task 5.1: myapp_run startup sequence ────────────────────────────────
*
* Req 1.1, 9.3, 9.4: myapp_run must call ha_start_election_async (non-blocking)
* and then cluster_run without any intervening blocking calls.
*
* 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.
* 3. ha_start_election_async completes in < 10ms — confirming myapp_run does not
* block before entering cluster_run.
*
* These tests are intentionally thin: myapp_run itself is an integration entry
* point that requires a live network socket. The unit tests here verify that
* the building blocks behave correctly; the integration test is test 13 in the
* design testing strategy (regression: single-node self-elects).
*/
static ha_config_t g_startup_cfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.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)
{
/* Req 1.1, 9.3: the new myapp_run path calls ha_start_election_async with
* n_peers=1 for a single-node cluster. This test verifies the call stores
* n_peers in ha->n_peers, matching the new myapp_run contract. */
ha_state_t ha;
ha_init(&ha, &g_startup_cfg, 0);
ha.n_peers = 0;
ha.phase2_done = 0;
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.n_peers,
"myapp_run path: ha_start_election_async must store n_peers=1 for single-node");
ha_fini(&ha);
printf("test_nonblocking_single_node_election_sets_n_peers: PASS\n");
}
static void test_nonblocking_single_node_election_enters_committing(void)
{
/* Req 9.3, 8.1: After ha_start_election_async(n_peers=1) the role must be
* CTRL_PRIMARY_COMMITTING, NOT CTRL_PRIMARY.
* This validates that the new myapp_run path (without blocking for(;;) loop)
* correctly transitions through the COMMITTING state. */
ha_state_t ha;
ha_init(&ha, &g_startup_cfg, 0);
ha.phase2_done = 0;
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
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_COMMITTING, (int)role,
"myapp_run path: single-node must enter CTRL_PRIMARY_COMMITTING after election");
ha_fini(&ha);
printf("test_nonblocking_single_node_election_enters_committing: PASS (role=%d)\n",
(int)role);
}
static void test_nonblocking_election_returns_in_under_10ms(void)
{
/* Req 9.4, 1.1: ha_start_election_async must return in < 5ms (design spec).
* Observable: myapp_run exits into cluster_run within 5ms of startup;
* no blocking calls visible between ha_start_election_async and cluster_run. */
ha_state_t ha;
ha_init(&ha, &g_startup_cfg, 0);
ha.phase2_done = 0;
struct timespec t0;
clock_gettime(CLOCK_MONOTONIC, &t0);
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
struct timespec t1;
clock_gettime(CLOCK_MONOTONIC, &t1);
long elapsed_ms_val = (t1.tv_sec - t0.tv_sec) * 1000L
+ (t1.tv_nsec - t0.tv_nsec) / 1000000L;
TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(5, (int)elapsed_ms_val,
"myapp_run path: ha_start_election_async must return in < 5ms (design spec)");
ha_fini(&ha);
printf("test_nonblocking_election_returns_in_under_10ms: PASS (%ldms)\n",
elapsed_ms_val);
}
/* ── Test suite entry point ───────────────────────────────────────────── */
void test_ha_startup(void)
@ -634,4 +739,9 @@ void test_ha_startup(void)
/* 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 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_election_returns_in_under_10ms);
}