forked from lei.chen/myxce
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:
parent
b2598823c0
commit
c9154bd5cc
6 changed files with 92 additions and 11 deletions
|
|
@ -1513,11 +1513,21 @@ static bool ha_has_write_quorum(ha_state_t *ha)
|
|||
if (ha->n_peers <= 1)
|
||||
return true;
|
||||
|
||||
/* Count connected standbys. standby_endpoints is populated by
|
||||
* raft_cb_become_leader (promoted vote eps) and on_standby_connected
|
||||
* (late joiners); removed by error handlers. */
|
||||
int connected = ha->standby_endpoints
|
||||
/* Count standbys able to receive new log entries. When the replicator
|
||||
* back-pointer is wired (task 5.3 init), use registry truth: slots with
|
||||
* ep != NULL && !in_catch_up under r->lock (Req 3.1-3.3) — catch-up
|
||||
* members cannot ACK new entries, so they must not count toward write
|
||||
* admission. NULL back-pointer (legacy contexts) falls back to the
|
||||
* standby_endpoints count populated by raft_cb_become_leader (promoted
|
||||
* vote eps) and on_standby_connected (late joiners); removed by error
|
||||
* handlers. */
|
||||
int connected;
|
||||
if (ha->repl) {
|
||||
connected = repl_active_standby_count(ha->repl);
|
||||
} else {
|
||||
connected = ha->standby_endpoints
|
||||
? ha->standby_endpoints->count : 0;
|
||||
}
|
||||
|
||||
/* Need at least n_peers/2 connected standbys to achieve quorum.
|
||||
* Integer division: n_peers/2 for n_peers=3 → 1 standby minimum. */
|
||||
|
|
|
|||
|
|
@ -192,6 +192,12 @@ typedef struct {
|
|||
uint64_t standby_reported_lsn[MAX_HA_CANDIDATES]; /* Last applied_lsn reported by each standby */
|
||||
bool standby_converged[MAX_HA_CANDIDATES]; /* Whether standby[i] has converged with primary */
|
||||
int peer_connect_fail_count[MAX_HA_CANDIDATES]; /* throttle log spam on repeated connect failure */
|
||||
/* ── New field added by ha-commit-engine spec (Task 4.2) ──────── */
|
||||
replicator_t *repl; /* Back-pointer to the standby registry (persist_t.repl).
|
||||
* When set, ha_has_write_quorum counts registry truth via
|
||||
* repl_active_standby_count (ep != NULL && !in_catch_up);
|
||||
* NULL → legacy standby_endpoints->count fallback.
|
||||
* (Req 3.1-3.3, 6.1, 6.2; wired at init by task 5.3.) */
|
||||
} ha_state_t;
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -550,12 +550,28 @@ int repl_needs_snapshot(replicator_t *r, uint64_t standby_lsn,
|
|||
* Write-admission quorum accounting (ha-commit-engine task 4)
|
||||
* ============================================================ */
|
||||
|
||||
/* RED scaffold for task 4.1: deliberately inert so the task-4.1 tests fail
|
||||
* on assertions rather than at link time (-1 is never a valid count).
|
||||
* The real counting — slots with ep != NULL && !in_catch_up under r->lock —
|
||||
* is implemented in task 4.2. */
|
||||
/* repl_active_standby_count — registry-truth count of standbys able to
|
||||
* receive new log entries: slots with ep != NULL && !in_catch_up, evaluated
|
||||
* under r->lock. NULL registry counts zero. Consumed by ha_has_write_quorum
|
||||
* (ha.c) for write admission. Takes ONLY r->lock — never the commit-engine
|
||||
* mutex (lock-ordering rule: gate path holds no locks when calling in).
|
||||
* (ha-commit-engine Req 3.1, 3.2, 3.3) */
|
||||
int repl_active_standby_count(replicator_t *r)
|
||||
{
|
||||
(void)r;
|
||||
return -1;
|
||||
int i;
|
||||
int count = 0;
|
||||
|
||||
if (!r)
|
||||
return 0;
|
||||
|
||||
mylock_lock(&r->lock);
|
||||
for (i = 0; i < MAX_STANDBYS; i++) {
|
||||
if (r->standbys[i].ep != NULL &&
|
||||
!atomic_load_explicit(&r->standbys[i].in_catch_up,
|
||||
memory_order_acquire))
|
||||
count++;
|
||||
}
|
||||
mylock_unlock(&r->lock);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue