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
|
|
@ -47,7 +47,7 @@
|
|||
- Observable completion: new cases FAIL (red) — current `ha_has_write_quorum` counts `standby_endpoints->count` including catch-up (grep ha.c for the current anchor near ha.c:1507)
|
||||
- _Requirements: 3.1, 3.2, 3.3, 6.1, 6.2_
|
||||
|
||||
- [ ] 4.2 Implement the quorum-accounting fix to green
|
||||
- [x] 4.2 Implement the quorum-accounting fix to green (incl. reviewer-mandated wired gate test 29; reviewed in main context — reviewer subagent unavailable)
|
||||
- Add `repl_active_standby_count(replicator_t *r)` to repl.c (under `r->lock`); add `replicator_t *repl` back-pointer to `ha_state_t` (ha.h); `ha_has_write_quorum` uses the helper when the back-pointer is wired, falls back to the existing `standby_endpoints` count when NULL (legacy test contexts); quorum condition itself unchanged (`active >= n_peers / 2`, `n_peers <= 1 → true`)
|
||||
- Observable completion: all task 4.1 tests PASS; existing dispatch-gate tests still green
|
||||
- _Requirements: 3.1, 3.2, 3.3, 6.1, 6.2_
|
||||
|
|
|
|||
|
|
@ -902,3 +902,11 @@
|
|||
| 16:20 | Adversarial review of ha-commit-engine task 4.1 (RED quorum tests): boundary clean, 5 RED + 3 pins verified, full census 8 fails (3 bug-411 + 5 RED), APPROVED | test_commit_engine.c, repl.h, repl.c | approved | ~25k |
|
||||
| 09:14 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~17 |
|
||||
| 09:15 | Edited .kiro/specs/ha-commit-engine/tasks.md | 1→2 lines | ~280 |
|
||||
| 09:19 | Edited src/controller/ha.h | added error handling | ~214 |
|
||||
| 09:19 | Edited src/controller/testsuite/test_commit_engine.c | added error handling | ~544 |
|
||||
| 09:20 | Edited src/controller/testsuite/test_commit_engine.c | added error handling | ~60 |
|
||||
| 09:23 | Edited src/controller/repl.c | added 2 condition(s) | ~223 |
|
||||
| 09:23 | Edited src/controller/ha.c | added error handling | ~202 |
|
||||
| 2026-06-11 | ha-commit-engine task 4.2: replaced repl_active_standby_count -1 stub with registry-truth count (ep!=NULL && !in_catch_up under r->lock); added ha_state_t.repl back-pointer (ha.h); ha_has_write_quorum uses helper when wired, legacy standby_endpoints fallback when NULL; added wired gate test 29 (RED: Expected -10013 Was 0 → GREEN) | src/controller/repl.c, ha.c, ha.h, testsuite/test_commit_engine.c | 29 tests 0 failures; full census = exactly 3 bug-411 baseline failures unchanged | ~60k |
|
||||
| 16:40 | Adversarial review ha-commit-engine task 4.2 (quorum-accounting fix): boundary clean, mutant run proves test 29 killing power (Expected -10013 Was 0), 29/29 group PASS, full census = exactly 3 bug-411 baseline failures, no sanitizer output — APPROVED | ha.c, ha.h, repl.c, test_commit_engine.c | approved | ~30k |
|
||||
| 15:28 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~42 |
|
||||
|
|
|
|||
|
|
@ -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