feat(ha-term-safety): on_commit_advanced barrier trigger + commit-gate ownership (task 2.4)

This commit is contained in:
Rongsong Shen 2026-06-22 07:33:04 +00:00
parent 2c5d298cb5
commit 3470bdfb71
6 changed files with 363 additions and 9 deletions

View file

@ -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.

View file

@ -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) {

View file

@ -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() /

View file

@ -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
/*

View file

@ -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);

View file

@ -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);
}