diff --git a/.kiro/specs/ha-term-safety/tasks.md b/.kiro/specs/ha-term-safety/tasks.md index 8e39ad3..69ddffb 100644 --- a/.kiro/specs/ha-term-safety/tasks.md +++ b/.kiro/specs/ha-term-safety/tasks.md @@ -50,7 +50,7 @@ - _Requirements: 1.3, 3.1_ - _Depends: 1.1_ -- [ ] 2.4 Implement `on_commit_advanced` as a trigger into the existing barrier, and resolve the commit-gate ownership +- [x] 2.4 Implement `on_commit_advanced` as a trigger into the existing barrier, and resolve the commit-gate ownership - Feed the library-supplied `commit_index` into the existing synchronous commit barrier (`commit_advance_locked`/`commit_set_writable_lsn`) as the commit SOURCE; the barrier mechanism (majority self-cap at durable floor, persist-before-ACK) is unchanged - Resolve double-gating (REVIEW ISSUE 3): state and implement whether the controller's existing own-term `WAL_NOOP` writable gate is now redundant with the library's current-term commit rule (and removed) or retained as a complementary durability gate; ensure the two cannot deadlock (commit held by both) or leave a gap - Observable completion: a strict library commit advance releases the barrier's WAIT only on durable majority; the term-blind safe-commit is no longer the commit source; a unit test pins the own-term-gate decision @@ -159,3 +159,8 @@ - **2.3 done.** on_send_append_entries → repl_send_append_entries: builds a WalCatchupEntry AE to standbys[peer_idx].ep carrying prev_log_index/prev_log_term; for n_entries==1 sources the entry at prev_log_index+1 from the local WAL (term via persist_entry_term, type+payload via ha_read_wal_entry_at); heartbeat = n_entries==0 (zero entries). Async (xce_wal_batch_req_raw), payload freed on all paths. 2 tests via repl_send_ae_capture_hook seam. Reviewer APPROVED. - **DEFERRED to task 3.2:** leader_commit is accepted+captured but NOT yet wire-encoded — WalCatchupEntry needs a `leaderCommit` capnp field + standby intake of it (3.2). Comment at repl.c corrected to state this. - **LOW (future refactor):** ha_read_wal_entry_at duplicates WAL segment-file parsing already in persist.c/config_repl.c/waldump.c — a persist_read_entry_at() accessor would consolidate; correct as written. + +- **2.4 done.** on_commit_advanced (ha.c `ha_cb_commit_advanced_stub` body, still named `*_stub` per the 2.1–2.3 convention) routes the library `commit_index` into the barrier via NEW `commit_set_raft_commit_index(ce, commit_index)` (commit.c) → `ce->raft_commit_index` ceiling. `majority_candidate_locked` now caps `committed = min(durable-majority, raft_commit_index)` on BOTH the standalone (n_peers<=1) and multi-peer paths. Own-term WAL_NOOP writable gate **RETAINED** (design 193/201/228) — proven non-conflicting: `writable_lsn` is only a WAIT predicate (`wait_done_locked`), `raft_commit_index` only caps `committed_lsn` — disjoint quantities, no deadlock; `committed=min(...)` ⇒ no gap; ceiling only ever lowers `cap` (starts at durable floor) ⇒ never over-commits; `assert(cand<=persist_durable_lsn)` still holds. Monotonic: setter raises only on `commit_index > raft_commit_index`. 5 tests; reviewer APPROVED (stash-and-rebuild baseline proved test_repl_unit 6-fail + 1 timeout are pre-existing bug-924/working-dir env, not regressions). + - **FEATURE GATE (critical for 3.x):** ceiling is INERT while `raft_commit_index == 0` (no on_commit_advanced fired). Lowest real LSN is 1 (commit_submit rejects lsn==0), so `0` unambiguously means "nothing committed" — no 0-vs-unset window. Tasks 3.1–3.3 wiring `raft_advance_commit` on the ACK path will fire the FIRST `on_commit_advanced(commit_index>=1)` and engage the library source; until then the autonomous barrier is preserved (no steady-state regression, R5.4). + - **DEFERRED to 3.x (decommission the term-blind source):** `ha_compute_safe_committed_lsn` is no longer the commit source but is NOT yet removed — it still drives the promotion-time safe truncate (R5). 3.x must ensure no path still feeds it INTO the barrier as a commit source once `raft_advance_commit` is wired. + - For-test seam: `ha_cb_commit_advanced_for_test` (ha.h, XCE_TESTING) + `commit_set_raft_commit_index` are the hooks 5.1 integration tests should reuse. diff --git a/src/controller/commit.c b/src/controller/commit.c index c2f039d..95c3f65 100644 --- a/src/controller/commit.c +++ b/src/controller/commit.c @@ -94,9 +94,30 @@ static uint64_t majority_candidate_locked(commit_engine_t* ce) int n = 0; int need, k, i; + /* ha-term-safety task 2.4: the raft library's current-term-gated commit + * index is the COMMIT SOURCE. Once fed, it is an ADDITIONAL ceiling on top + * of the durable cap: committed_lsn = min(durable-majority, raft_commit_index), + * so the barrier never advances committed over an entry the library has not + * admitted under its Figure-8 current-term rule (replacing the term-blind + * safe-commit). Folding it into `cap` makes BOTH the standalone (n_peers<=1) + * and the multi-peer paths honor the source uniformly. + * + * FEATURE GATE: raft_commit_index == 0 means the library commit source is + * NOT yet active (no on_commit_advanced has fired — e.g. before tasks 3.x + * drive raft_advance_commit, or in a node that has committed nothing). In + * that state the ceiling is INERT and the barrier keeps its autonomous + * durable-majority behavior; the source only takes over once the library + * reports its first strict commit (commit_index >= 1, which is the lowest + * real LSN). This is the stub→real transition for this callback. */ + if (ce->raft_commit_index != 0 && ce->raft_commit_index < cap) { + cap = ce->raft_commit_index; + } + /* Standalone / single-member: self constitutes the majority (Req 6.3) — - * but self still only counts up to what is durable locally, so a - * single-member commit also cannot advance over a not-yet-synced entry. */ + * but self still only counts up to what is durable locally AND what the + * library has committed (cap already folds in raft_commit_index), so a + * single-member commit also cannot advance over a not-yet-synced or + * not-yet-library-committed entry. */ if (ce->n_peers <= 1) { return cap; } @@ -249,6 +270,7 @@ int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcp ce->n_vcpus = n_vcpus; ce->inflight_waits = 0; ce->writable_lsn = 0; /* no own-term gate until set by promotion */ + ce->raft_commit_index = 0; /* no library commit source until fed (task 2.4) */ memset(ce->scratch, 0, sizeof(ce->scratch)); /* Register the durable-advance wake so the flusher can re-trigger commit @@ -394,6 +416,35 @@ void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn) pthread_mutex_unlock(&ce->lock); } +/* ha-term-safety task 2.4: feed the raft library's current-term-gated commit + * index as the commit SOURCE (R4.1, R4.5). Monotonic max() — the library only + * fires on_commit_advanced on a strict advance, but guard against a stale/lower + * value so the ceiling never regresses. Re-evaluate the barrier under the new + * ceiling and wake waiters: the durable cap may already cover the newly-admitted + * LSNs, so the commit can advance immediately without waiting for the next ACK. + * + * LOCK ORDERING: takes only ce->lock (commit_advance_locked re-reads the durable + * watermark + registry watermarks via the existing path). The on_commit_advanced + * callback runs on the iomgr ACK path that drove raft_advance_commit; that path + * does not hold p->lock here, preserving the p->lock → ce->lock direction. */ +void commit_set_raft_commit_index(commit_engine_t* ce, uint64_t commit_index) +{ + if (ce == NULL) { + return; + } + pthread_mutex_lock(&ce->lock); + if (commit_index > ce->raft_commit_index) { + ce->raft_commit_index = commit_index; /* monotonic ceiling */ + } + /* The ceiling moved (or was confirmed): re-derive the majority under it and + * advance committed_lsn if the durable majority already covers the newly + * admitted prefix. commit_advance_locked broadcasts on any advance; broadcast + * unconditionally so a waiter re-checks even on a no-advance ceiling bump. */ + commit_advance_locked(ce); + pthread_cond_broadcast(&ce->cond); + pthread_mutex_unlock(&ce->lock); +} + void commit_on_membership_change(commit_engine_t* ce) { if (ce == NULL || ce->p == NULL) { diff --git a/src/controller/ha.c b/src/controller/ha.c index bd5dcd6..f9477c8 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -1334,10 +1334,10 @@ static void raft_cb_trace(void *ctx, const char *event, const char *detail) * they perform NO persistence, NO replication, and NO truncation. See CONCERNS. * * on_persist_entry → task 2.1 (durable WAL append/overwrite) — IMPLEMENTED - * on_get_entry_term → task 2.2 (WAL entry-term lookup; persist_entry_term, 1.3) - * on_send_append_entries→ task 2.3 (AppendEntries transmit) - * on_truncate_from → task 2.4 (conflict truncation) - * on_commit_advanced → task 2.5 (commit-barrier trigger) + * on_get_entry_term → task 2.2 (WAL entry-term lookup; persist_entry_term, 1.3) — IMPLEMENTED + * on_send_append_entries→ task 2.3 (AppendEntries transmit) — IMPLEMENTED + * on_truncate_from → task 2.2 (conflict truncation) — IMPLEMENTED + * on_commit_advanced → task 2.4 (commit-barrier trigger / commit source) — IMPLEMENTED * * on_get_entry_term returns 0 (== "absent") for now: with the log-replication * path unwired, the library's local last-log term reads as 0, so the logOk gate @@ -1612,10 +1612,30 @@ static int ha_cb_truncate_from_stub(void *ctx, uint64_t index) return persist_truncate_wal_above(&ha->raft_c->persist, index - 1); } +/* on_commit_advanced (ha-term-safety task 2.4, R4.1/R4.5/R5.4): feed the raft + * library's current-term-gated `commit_index` into the existing synchronous + * commit barrier as the commit SOURCE. The library fired this ONLY on a strict + * advance of its Figure-8 current-term commit index (raft.c), so the barrier + * now advances committed_lsn only when BOTH a durable majority agrees AND the + * library has admitted the entry — the term-blind safe-commit + * (ha_compute_safe_committed_lsn) is no longer the steady-state commit source. + * + * This callback only TRIGGERS the barrier (records the ceiling + re-evaluates); + * it performs NO durability itself. The barrier mechanism — majority self-cap at + * the durable floor, persist-before-ACK, monotonic committed_lsn — is unchanged + * (ha-commit-durability). The own-term writable_lsn gate is RETAINED as a + * complementary WAIT-completion predicate (it never caps committed_lsn, so it + * cannot deadlock against this source; see ha_arm_own_term_gate). + * + * Non-blocking (ha-async-io): commit_set_raft_commit_index takes only ce->lock + * and never performs I/O — safe on the iomgr ACK path that drove + * raft_advance_commit. */ static void ha_cb_commit_advanced_stub(void *ctx, uint64_t commit_index) { - (void)ctx; (void)commit_index; - /* TODO task 2.5: trigger the existing synchronous commit barrier. */ + ha_state_t *ha = (ha_state_t *)ctx; + if (!ha || !ha->raft_c) + return; /* test mode without a cluster: no barrier to trigger */ + commit_set_raft_commit_index(&ha->raft_c->commit, commit_index); } static const raft_callbacks_t s_real_cbs = { @@ -1838,6 +1858,14 @@ int ha_cb_send_append_entries_for_test(ha_state_t *ha, int peer_idx, prev_log_term, leader_commit, n_entries); } + +/* ha-term-safety task 2.4: drive the WIRED on_commit_advanced callback so a unit + * test exercises the real commit-source trigger (feed the library commit_index + * into the cluster's commit barrier). Exposed only under XCE_TESTING. */ +void ha_cb_commit_advanced_for_test(ha_state_t *ha, uint64_t commit_index) +{ + s_real_cbs.on_commit_advanced(ha, commit_index); +} #endif /* ha_check_heartbeat_nocluster() / ha_watchdog_thread() / ha_start_watchdog() / diff --git a/src/controller/ha.h b/src/controller/ha.h index 5827da7..97930d7 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -803,6 +803,15 @@ int ha_cb_send_append_entries_for_test(ha_state_t *ha, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, uint64_t leader_commit, int n_entries); + +/* + * ha_cb_commit_advanced_for_test (ha-term-safety task 2.4): invoke the WIRED + * on_commit_advanced callback directly so a unit test exercises the real + * commit-source trigger — feeding the raft library's current-term-gated + * commit_index into the cluster's synchronous commit barrier as the commit + * source. TEST-ONLY. + */ +void ha_cb_commit_advanced_for_test(ha_state_t *ha, uint64_t commit_index); #endif /* diff --git a/src/controller/include/commit.h b/src/controller/include/commit.h index 3f82f2f..7b4f881 100644 --- a/src/controller/include/commit.h +++ b/src/controller/include/commit.h @@ -31,6 +31,15 @@ typedef struct commit_engine { int n_vcpus; int inflight_waits; /* <= max(1, n_vcpus - COMMIT_WAIT_RESERVED) */ uint64_t writable_lsn; /* own-term gate; 0 = no gate */ + /* ha-term-safety task 2.4: the raft library's current-term-gated commit + * index — the COMMIT SOURCE (replaces the term-blind safe-commit). The + * barrier never advances committed_lsn past this ceiling, so an entry is + * committed only when BOTH a durable majority agrees AND the library's + * Figure-8 current-term commit rule has admitted it. 0 = no library commit + * index yet (nothing committed) — committed_lsn stays at 0. Monotonic + * non-decreasing (the library only fires on_commit_advanced on a strict + * advance), set under ce->lock via commit_set_raft_commit_index. */ + uint64_t raft_commit_index; uint64_t scratch[MAX_STANDBYS]; /* sorted matchIndex scratch array */ } commit_engine_t; @@ -56,6 +65,13 @@ void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn); * (API ships here; no caller in this spec — unset = no gate.) */ void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn); +/* ha-term-safety task 2.4: feed the raft library's current-term-gated commit + * index as the commit SOURCE. Records the ceiling (monotonic max() — never + * regresses) and re-evaluates the barrier under the new ceiling, advancing + * committed_lsn and waking satisfied waiters. Called from the on_commit_advanced + * callback after the library admits a strict commit advance. Never blocks. */ +void commit_set_raft_commit_index(commit_engine_t* ce, uint64_t commit_index); + /* Quorum-membership change (standby add/remove/catch-up transition): * re-evaluate pending entries; standbys in catch-up are excluded. */ void commit_on_membership_change(commit_engine_t* ce); diff --git a/src/controller/testsuite/test_term_safety_adapter.c b/src/controller/testsuite/test_term_safety_adapter.c index 56b8eea..dbf2bfc 100644 --- a/src/controller/testsuite/test_term_safety_adapter.c +++ b/src/controller/testsuite/test_term_safety_adapter.c @@ -13,6 +13,7 @@ #include "persist.h" #include "wal_types.h" #include "repl.h" +#include "commit.h" #include "xce/cluster_def.h" #include "ha.h" @@ -621,6 +622,245 @@ static void test_send_append_entries_heartbeat_zero_entries(void) printf("test_send_append_entries_heartbeat_zero_entries: PASS\n"); } +/* ================================================================== */ +/* Task 2.4 — on_commit_advanced trigger + commit-source switch */ +/* ================================================================== */ + +/* + * Task 2.4 feeds the library-supplied current-term-gated `commit_index` into + * the existing synchronous commit barrier as the commit SOURCE. The barrier + * mechanism is unchanged: committed_lsn still advances only when self+majority + * agree AND the bytes are durable locally (durable_lsn cap). The library commit + * index becomes an ADDITIONAL ceiling so the barrier never advances committed + * past the library's current-term-safe point. on_commit_advanced only TRIGGERS + * (records the ceiling + re-evaluates); it never performs durability itself. + * + * These cases build the commit engine standalone over a stub persist_t + + * replicator (the test_commit_engine.c pattern) so the barrier interaction is + * tested in isolation, plus one case driving the WIRED on_commit_advanced + * callback end-to-end through ha_cb_commit_advanced_for_test. + * + * RED: against the no-op ha_cb_commit_advanced_stub (and the missing + * commit_set_raft_commit_index ceiling), the library commit index is NOT a + * commit source — committed_lsn would advance on durable-majority alone with + * no current-term gate. GREEN: the wired callback feeds the ceiling so the + * barrier holds until BOTH durable majority and the library commit index agree. + */ + +/* Stub-persist fixture for the commit-source tests (mirrors test_commit_engine.c). */ +static persist_t g_t24_p; +static commit_engine_t g_t24_ce; +static int g_t24_eps[MAX_STANDBYS]; + +static void t24_engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn) +{ + char name[16]; + memset(&g_t24_p, 0, sizeof(g_t24_p)); + TEST_ASSERT_EQUAL_INT(0, repl_init(&g_t24_p.repl)); + for (int i = 0; i < n_standbys; i++) { + snprintf(name, sizeof(name), "sb%d", i); + TEST_ASSERT_EQUAL_INT( + i, repl_add_standby(&g_t24_p.repl, &g_t24_eps[i], name, + 7000 + (uint32_t)i)); + } + g_t24_p.current_lsn = appended_lsn + 1; + atomic_store(&g_t24_p.committed_lsn, 0); + atomic_store(&g_t24_p.durable_lsn, appended_lsn); + TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_t24_ce, &g_t24_p, n_peers, 8)); +} + +static void t24_engine_teardown(void) +{ + commit_engine_fini(&g_t24_ce); + repl_fini(&g_t24_p.repl); +} + +static void t24_standby_ack(int idx, uint64_t acked_lsn) +{ + atomic_store(&g_t24_p.repl.standbys[idx].acked_lsn, acked_lsn); + commit_on_ack(&g_t24_ce, idx, acked_lsn); +} + +static uint64_t t24_committed(void) +{ + return atomic_load(&g_t24_p.committed_lsn); +} + +/* Test: the library commit index is the commit SOURCE once fed. A durable + * majority covers LSN 3, but the library has only admitted (current-term commit) + * up to LSN 1, so the barrier caps committed at 1 — NOT at the durable-majority + * floor of 3. Raising the library ceiling to 3 then releases the barrier to 3. + * This proves the library current-term commit index gates the commit, replacing + * the term-blind safe-commit (R4.1, R4.5, R5.4). + * + * (raft_commit_index == 0 is the INERT pre-wire state — the library source is + * not yet active and the barrier keeps autonomous durable-majority behavior; + * that flag-OFF fallback is covered by the existing test_commit_engine suite, + * whose autonomous semantics this task must not regress.) */ +static void test_commit_advanced_is_commit_source(void) +{ + /* 3 peers = self + 2 standbys; majority needs self + 1 standby. */ + t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/3); + + /* The library is the commit source from the first commit: establish the + * ceiling at LSN 1 BEFORE any ACK drives evaluation (production order — the + * ACK path drives raft_advance_commit, which fires on_commit_advanced, then + * the barrier re-evaluates). */ + commit_set_raft_commit_index(&g_t24_ce, 1); + + /* A standby ACKs LSN 3; self is durable to LSN 3 → durable majority = 3. + * The barrier must cap committed at the library ceiling (1), NOT at the + * durable majority (3). */ + t24_standby_ack(0, 3); + TEST_ASSERT_EQUAL_UINT64(1, t24_committed()); + + /* Library admits up to LSN 3 → barrier releases to the durable majority (3). */ + commit_set_raft_commit_index(&g_t24_ce, 3); + TEST_ASSERT_EQUAL_UINT64(3, t24_committed()); + + t24_engine_teardown(); + printf("test_commit_advanced_is_commit_source: PASS\n"); +} + +/* Test: the library commit index is a CEILING — committed never advances past + * it even when a durable majority covers a higher LSN (current-term gate). The + * barrier's durable cap is the OTHER ceiling: committed = min(durable-majority, + * raft_commit_index). No gap: raising either ceiling alone cannot overshoot the + * other (R4.1, R4.5). */ +static void test_commit_advanced_ceiling_caps_durable_majority(void) +{ + t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/5); + + /* Establish the library ceiling at 3 BEFORE any ACK (production order). The + * library has only committed up to 3 (higher entries prev-term or not yet + * current-term-agreed). */ + commit_set_raft_commit_index(&g_t24_ce, 3); + + /* Durable majority covers up to LSN 5 (self durable 5 + standby acked 5). + * committed must cap at the library ceiling (3), not the durable floor (5). */ + t24_standby_ack(0, 5); + TEST_ASSERT_EQUAL_UINT64(3, t24_committed()); + + /* Library advances the ceiling to 5 → committed catches up to the durable + * majority floor (5). Monotonic, no regression. */ + commit_set_raft_commit_index(&g_t24_ce, 5); + TEST_ASSERT_EQUAL_UINT64(5, t24_committed()); + + t24_engine_teardown(); + printf("test_commit_advanced_ceiling_caps_durable_majority: PASS\n"); +} + +/* Test: the library commit index NEVER lets committed advance past what is + * durable locally. With raft_commit_index=4 but self only durable to LSN 2 and + * the standby acked LSN 2, committed must stay at the durable-majority floor (2) + * — the library ceiling does not bypass persist-before-ACK / durable cap + * (R5.4 — barrier durability mechanism unchanged). */ +static void test_commit_advanced_does_not_bypass_durable_cap(void) +{ + t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/4); + atomic_store(&g_t24_p.durable_lsn, 2); /* self durable only to 2 */ + + t24_standby_ack(0, 2); /* standby agrees to 2 */ + commit_set_raft_commit_index(&g_t24_ce, 4); /* library ceiling well ahead */ + commit_on_ack(&g_t24_ce, 0, 2); + + /* Durable-majority floor is 2; the library ceiling (4) cannot raise it. */ + TEST_ASSERT_EQUAL_UINT64(2, t24_committed()); + + t24_engine_teardown(); + printf("test_commit_advanced_does_not_bypass_durable_cap: PASS\n"); +} + +/* Test (pins the own-term-gate DECISION — RETAINED, complementary, no deadlock): + * the own-term writable_lsn gate is a WAIT-completion predicate, NOT a cap on + * committed_lsn. It is retained alongside the library commit source. With the + * gate armed at LSN 2 and the library commit index + durable majority both at 2, + * committed advances to 2 AND a WAIT(lsn=2) completes — the two gates cannot + * deadlock (the own-term gate never holds committed_lsn back; the library + * ceiling never holds the WAIT predicate back beyond committed_lsn). */ +static void test_own_term_gate_retained_no_deadlock(void) +{ + t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/2); + + /* Arm the own-term gate at LSN 2 (retained complementary durability gate). */ + commit_set_writable_lsn(&g_t24_ce, 2); + + /* Library commit source + durable majority both reach LSN 2. */ + t24_standby_ack(0, 2); + commit_set_raft_commit_index(&g_t24_ce, 2); + commit_on_ack(&g_t24_ce, 0, 2); + + /* committed_lsn advanced to 2 (own-term gate never caps committed_lsn). */ + TEST_ASSERT_EQUAL_UINT64(2, t24_committed()); + + /* A WAIT for LSN 2 completes immediately: committed_lsn (2) >= lsn (2) AND + * >= writable_lsn (2). No deadlock between the gate and the library source. */ + TEST_ASSERT_EQUAL_INT(0, + commit_submit(&g_t24_ce, /*lsn*/2, COMMIT_MODE_WAIT, /*timeout_ms*/500)); + + t24_engine_teardown(); + printf("test_own_term_gate_retained_no_deadlock: PASS\n"); +} + +/* Test: the WIRED on_commit_advanced callback routes commit_index into the + * cluster's commit engine end-to-end (ha_cb_commit_advanced_for_test) as the + * commit SOURCE. A durable majority covers LSN 2, but the wired callback feeds + * commit_index=1 first → the barrier caps committed at 1 (library current-term + * gate), NOT at the durable majority of 2. A second callback at commit_index=2 + * releases the barrier to 2. Proves the stub→real transition: the no-op stub + * never fed the source, so committed would have followed the durable majority. */ +static void test_on_commit_advanced_wired_feeds_barrier(void) +{ + char tmp[256]; + strcpy(tmp, "/tmp/xce_ts_commitadv_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(tmp)); + + cluster_t *c = make_cluster(tmp); + TEST_ASSERT_NOT_NULL(c); + + /* Init the cluster's embedded commit engine over the same persist as a + * 3-peer cluster (self + 2 standbys), so a majority is self + 1 standby. */ + TEST_ASSERT_EQUAL_INT(0, + commit_engine_init(&c->commit, &c->persist, /*n_peers*/3, /*n_vcpus*/8)); + + /* Persist two entries BEFORE registering a standby — otherwise the primary + * own-append fans out via repl_broadcast to the dummy int ep (ASAN + * global-buffer-overflow reading ep->peer); with no standby the broadcast + * is a no-op. Append LSN 1,2 with term 4 → durable floor reaches LSN 2. */ + atomic_store(&c->ha.role, CTRL_PRIMARY); + uint8_t blob[64]; + size_t n1 = pack_entry(blob, WAL_JOB_UPDATE, "a", 1); + TEST_ASSERT_EQUAL_INT(0, ha_cb_persist_entry_for_test(&c->ha, 1, 4, blob, n1)); + size_t n2 = pack_entry(blob, WAL_JOB_UPDATE, "b", 1); + TEST_ASSERT_EQUAL_INT(0, ha_cb_persist_entry_for_test(&c->ha, 2, 4, blob, n2)); + TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist)); + + /* Now register the standby so a majority (self + 1) is reachable. The ACK + * below is driven directly into the registry + commit_on_ack (no network). */ + TEST_ASSERT_EQUAL_INT(0, + repl_add_standby(&c->persist.repl, &g_ae_dummy_ep, "sb0", 7000)); + + /* Fire the WIRED callback FIRST to establish the library ceiling at LSN 1 + * (production order: the ACK path drives raft_advance_commit, which fires + * on_commit_advanced before/with the barrier re-evaluation). */ + ha_cb_commit_advanced_for_test(&c->ha, 1); + + /* Standby ACKs LSN 2 → durable majority covers LSN 2. The wired source caps + * committed at 1, BELOW the durable majority (2): the library current-term + * gate governs (the term-blind safe-commit is retired as the source). */ + atomic_store(&c->persist.repl.standbys[0].acked_lsn, 2); + commit_on_ack(&c->commit, 0, 2); + TEST_ASSERT_EQUAL_UINT64(1, atomic_load(&c->persist.committed_lsn)); + + /* Library admits LSN 2 → barrier releases to the durable majority (2). */ + ha_cb_commit_advanced_for_test(&c->ha, 2); + TEST_ASSERT_EQUAL_UINT64(2, atomic_load(&c->persist.committed_lsn)); + + commit_engine_fini(&c->commit); + teardown_cluster(c); + printf("test_on_commit_advanced_wired_feeds_barrier: PASS\n"); +} + /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ @@ -640,4 +880,9 @@ void test_term_safety_adapter(void) RUN_TEST(test_on_truncate_from_removes_all); RUN_TEST(test_send_append_entries_one_entry_from_wal); RUN_TEST(test_send_append_entries_heartbeat_zero_entries); + RUN_TEST(test_commit_advanced_is_commit_source); + RUN_TEST(test_commit_advanced_ceiling_caps_durable_majority); + RUN_TEST(test_commit_advanced_does_not_bypass_durable_cap); + RUN_TEST(test_own_term_gate_retained_no_deadlock); + RUN_TEST(test_on_commit_advanced_wired_feeds_barrier); }