feat(ha-commit-engine): honest write-admission quorum accounting (task 4.2)

repl_active_standby_count: registry truth (ep != NULL && !in_catch_up)
under r->lock. ha_state_t gains repl back-pointer (wired at init by
task 5.3); ha_has_write_quorum uses registry truth when wired, exact
legacy fallback when NULL; formula unchanged. Test 29 (wired gate,
catch-up-only standby → NOT_READY, admits after clear) captured RED
pre-fix. 29/29 group tests; suite at the 3-failure baseline.
This commit is contained in:
Rongsong Shen 2026-06-11 22:28:44 +00:00
parent b2598823c0
commit c9154bd5cc
6 changed files with 92 additions and 11 deletions

View file

@ -1061,6 +1061,45 @@ static void test_gate_reaccepts_after_reconnect_without_reset(void)
printf("test_gate_reaccepts_after_reconnect_without_reset: PASS\n");
}
/* ── Test 29: WIRED gate — registry truth excludes catch-up at the gate ───
* Task 4.2 (reviewer-mandated RED for the gate half): n_peers=3 primary with
* the ha_state_t.repl back-pointer wired to a registry whose ONLY connected
* standby is in catch-up. standby_endpoints still lists the endpoint (as
* on_standby_connected would), so the legacy count says 1 — but the registry
* truth says 0 active. The gate must reject the write with NOT_READY; once
* catch-up clears, the very next dispatch is admitted (stateless, Req 6.2).
* (Req 3.1, 3.2, 6.1, 6.2) */
static void test_gate_wired_registry_catchup_standby_rejects_then_admits(void)
{
int rc_catchup, rc_cleared;
/* Arrange: 3-member primary; endpoint registered in standby_endpoints
* AND in the replicator registry, where it is marked in catch-up */
gate_setup(3, 1);
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_reg));
TEST_ASSERT_EQUAL_INT(0, repl_add_standby(&g_reg, &g_eps[0], "sb0", 7000));
repl_start_catchup(&g_reg, &g_eps[0], 0);
g_gate_ha.repl = &g_reg; /* task 5.3 wires this at init; set directly here */
/* Act: dispatch while the only standby is catching up... */
rc_catchup = ha_dispatch_gate(&g_gate_ha, OPCode_jobsub);
/* ...then catch-up completes — nothing else changes */
repl_complete_catchup(&g_reg, &g_eps[0]);
rc_cleared = ha_dispatch_gate(&g_gate_ha, OPCode_jobsub);
/* Assert: catch-up-only standby → no write quorum at the gate */
TEST_ASSERT_EQUAL_INT_MESSAGE(-XCE_ERR_NOT_READY, rc_catchup,
"wired gate must reject writes when the only standby is in catch-up");
/* ...and clearing catch-up re-admits with no reset call */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc_cleared,
"wired gate must admit writes once catch-up completes");
g_gate_ha.repl = NULL;
repl_fini(&g_reg);
gate_teardown();
printf("test_gate_wired_registry_catchup_standby_rejects_then_admits: PASS\n");
}
/* ── Test group entry point ──────────────────────────────────────────────── */
void test_commit_engine(void)
@ -1097,5 +1136,7 @@ void test_commit_engine(void)
RUN_TEST(test_gate_single_member_always_admits_writes);
RUN_TEST(test_gate_rejects_writes_with_zero_connected_standbys);
RUN_TEST(test_gate_reaccepts_after_reconnect_without_reset);
/* Task 4.2 — wired gate: registry truth at ha_dispatch_gate */
RUN_TEST(test_gate_wired_registry_catchup_standby_rejects_then_admits);
UNITY_END();
}