test(ha-commit-barrier-rollout): RED tests for persist_commit_barrier (task 2.1)

7 cases: two covered short-circuits, timeout with committed_lsn
unmoved, covering-ACK release, sync_timeout_ms fallback (bounded both
directions), n_peers=1 standalone, NOT_READY passthrough via cap
saturation. Watermark resolved to current_lsn - 1 (design's literal
phrasing unsatisfiable per persist.h:71 invariant) with off-by-one
killing power both ways. 15 tests / 7 RED; suite baseline zero.
This commit is contained in:
Rongsong Shen 2026-06-12 05:09:11 +00:00
parent e8ecc87551
commit 611fbc8d7b
6 changed files with 405 additions and 2 deletions

View file

@ -19,7 +19,7 @@
- _Requirements: 2.1, 2.2, 3.1_
- [ ] 2. Commit barrier helper and legacy adapter retirement
- [ ] 2.1 Write RED tests for persist_commit_barrier semantics
- [x] 2.1 Write RED tests for persist_commit_barrier semantics
- Declare `persist_commit_barrier(persist_t *p, uint32_t timeout_ms)` in src/include/persist.h
- Tests (stub engine + replicator, n_peers=3 unless noted): returns 0 immediately when `committed_lsn` already covers `current_lsn`; returns `-ETIMEDOUT` when ACKs are withheld past the timeout and `committed_lsn` stays unmoved; returns 0 once a covering ACK advances the commit index; `timeout_ms = 0` falls back to `p->config.sync_timeout_ms`; n_peers=1 returns 0 immediately (standalone preserved); `-XCE_ERR_NOT_READY` from the engine passes through unmodified
- Observable completion: new cases FAIL (red) — the helper does not exist yet
@ -112,3 +112,4 @@
- (1.1) wal_entry_type has 13 values (wal_types.h:44-58), no WAL_NOOP yet (lands in ha-single-leader-role, which must add a classification row + sweep entry — the totality sweep will catch it).
- (1.1) Suite baseline is ZERO failures since ha-commit-engine 6.1. detect_leaks=0 remains the harness norm (6 inventoried pre-existing leak groups).
- (1.2) CENSUS METHOD: only ~9 of 48 groups emit Unity summary blocks; the rest report per-test PASS lines only — count distinct Unity results (~465) and grep FAIL lines, never sum summary blocks. commit_submit(RETAIN) early-returns BEFORE taking ce->lock (commit.c:233-239) — registration is lock-free, safe with entity write locks held. wal_hook_append_classified is the single classified-append seam; appended_lsn read = current_lsn - 1 under p->lock (benign race, commented).
- (2.1) BARRIER WATERMARK RESOLVED (reviewer-judged): design's literal "Snapshot lsn = p->current_lsn" is loose phrasing contradicted by its own anchors — the coherent contract is watermark = current_lsn - 1 (last appended; persist.h:71 invariant committed_lsn <= current_lsn - 1 makes the literal reading unsatisfiable). Tests pin current_lsn - 1 with off-by-one killing power in BOTH directions; task 2.2 must implement that. Resolved contract recorded in persist.h doc comment.

View file

@ -82,3 +82,4 @@
- commit_submit(COMMIT_MODE_RETAIN) is REGISTRATION-FREE (commit.c:233-239, verified 2026-06-11): returns 0 immediately, records NOTHING per entry — commit state derives entirely from registry watermarks at ACK time. RETAIN routing at the hook seam therefore has NO engine-side observable distinct from the append itself; tests of RETAIN routing must assert classification + non-blocking return + lsn > committed_lsn pending + ACK-driven advancement, never a fake "registered" counter. (ha-commit-barrier-rollout 1.1)
- XCE_TESTING fail-fast (assert/abort) paths are testable via fork(): child silences stderr, calls the guarded fn, _exit(0); parent asserts WIFSIGNALED && WTERMSIG == SIGABRT. Precedent: lib testsuite test.c:1783; ASAN-compatible with detect_leaks=0. (ha-commit-barrier-rollout 1.1)
- ha_has_write_quorum (ha.c:1507) is STATIC — testsuite reaches it only through ha_dispatch_gate(ha, opcode) (extern-declared in test files, pattern: test_ha_repl_ack.c). Gate fixtures: ha_init + n_peers + atomic role store + heap zarray standby_endpoints (test_ha_connection_reuse.c pattern). Catch-up exclusion at the gate is untestable until the ha_state_t.repl back-pointer lands (ha-commit-engine 4.2) — assert via repl_active_standby_count instead. (2026-06-11, task 4.1)
- persist_commit_barrier watermark = current_lsn - 1 (matching persist.c:1021 persist_wal_sync_append read); covered ⇔ current_lsn == 0 OR committed_lsn >= current_lsn - 1. Design §Commit Barrier Helper "lsn = p->current_lsn" is shorthand — the parenthetical persist.c:1019-1021 reference pins it to current_lsn - 1 (engine clamps committed to highest appended, so waiting on current_lsn itself would never complete). (2026-06-12, task 2.1)

View file

@ -985,3 +985,12 @@
| 04:42 | Adversarial review of ha-commit-barrier-rollout task 1.2 (wal_classify + hook dispatch): APPROVED — full suite census 465 Unity PASS / 0 FAIL (implementer's 9/109 was a Unity-summary-block misread, not a partial run) | src/controller/persist.c | APPROVED | ~30k |
| 21:43 | Edited .kiro/specs/ha-commit-barrier-rollout/tasks.md | inline fix | ~19 |
| 21:43 | Edited .kiro/specs/ha-commit-barrier-rollout/tasks.md | 1→2 lines | ~168 |
| 21:49 | Edited src/include/persist.h | expanded (+15 lines) | ~287 |
| 21:50 | Edited src/controller/persist.c | modified contract() | ~230 |
| 21:50 | Edited src/controller/testsuite/test_commit_barrier_rollout.c | 4→5 lines | ~27 |
| 21:50 | Edited src/controller/testsuite/test_commit_barrier_rollout.c | 2→3 lines | ~51 |
| 21:50 | Edited src/controller/testsuite/test_commit_barrier_rollout.c | expanded (+13 lines) | ~332 |
| 21:51 | Edited src/controller/testsuite/test_commit_barrier_rollout.c | added 3 condition(s) | ~3683 |
| 05:05 | ha-commit-barrier-rollout task 2.1: RED tests for persist_commit_barrier (7 cases) + decl + inert -ENOSYS stub | test_commit_barrier_rollout.c, persist.h, persist.c | 7 FAIL (RED) on -38 sentinel, prior 8 PASS, suite census clean | ~60k |
| 22:08 | Edited .kiro/specs/ha-commit-barrier-rollout/tasks.md | inline fix | ~17 |
| 22:08 | Edited .kiro/specs/ha-commit-barrier-rollout/tasks.md | modified RESOLVED() | ~256 |

View file

@ -1171,6 +1171,21 @@ wal_commit_mode_t wal_classify(uint32_t wal_type, int detail)
}
}
/* Commit barrier helper — INERT RED stub (ha-commit-barrier-rollout task
* 2.1; implemented in task 2.2). Returns a wrong sentinel so the task-2.1
* tests fail on assertions, not linking. Task-2.2 contract (design.md
* §Commit Barrier Helper): snapshot the appended watermark (current_lsn - 1
* under p->lock), return 0 when committed_lsn already covers it (or nothing
* appended), else commit_submit(WAIT, timeout_ms ? timeout_ms :
* p->config.sync_timeout_ms) with the engine code passed through
* unmodified. */
int persist_commit_barrier(persist_t* p, uint32_t timeout_ms)
{
(void)p;
(void)timeout_ms;
return -ENOSYS;
}
/* Classified append at the hook seam (barrier-rollout task 1.2).
*
* WAIT (queue/worker control): the append stays async here — the handler

View file

@ -47,10 +47,24 @@
* repl_broadcast_send_hook to drop broadcasts) and test_persist_hooks.c
* (variadic trampoline driving persist_job_hook_fun with a real va_list).
*
* Requirements: 2.1, 2.2, 3.1
* Task 2.1 adds the persist_commit_barrier(p, timeout_ms) semantics cases
* (Req 1.1, 1.2, 1.3): the handler-callable WAIT barrier returns 0 without
* blocking when committed_lsn already covers the appended watermark
* (current_lsn - 1, the same snapshot persist_wal_sync_append reads — and
* trivially when nothing was ever appended); -ETIMEDOUT when ACKs are
* withheld past the deadline with committed_lsn unmoved; 0 once a covering
* watermark ACK advances the commit index; timeout_ms = 0 falls back to
* p->config.sync_timeout_ms; n_peers=1 succeeds immediately (engine
* self-quorum, standalone preserved); and the engine's admission-cap
* -XCE_ERR_NOT_READY passes through unmodified. RED against the task-2.1
* inert stub in persist.c (returns -ENOSYS — every case fails on its
* assertions, not linking); green with the task-2.2 implementation.
*
* Requirements: 1.1, 1.2, 1.3 (task 2.1); 2.1, 2.2, 3.1 (task 1.1)
*/
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdatomic.h>
@ -64,6 +78,7 @@
#include <unity/unity.h>
#include "xce/cluster_def.h" /* HOOK_DETAIL_*, XCE_CONTROLLER_HOOK_JOB */
#include "xce/errnox.h" /* XCE_ERR_NOT_READY (task 2.1 passthrough) */
#include "xce/types.h" /* job_t */
#include "commit.h"
@ -408,6 +423,345 @@ static void test_fact_hook_append_is_retained_and_nonblocking(void)
printf("test_fact_hook_append_is_retained_and_nonblocking: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 2.1 — persist_commit_barrier(p, timeout_ms) semantics
* (Req 1.1, 1.2, 1.3). RED against the inert -ENOSYS stub in persist.c.
*
* Fixture: stub persist_t + replicator + wired commit engine (pattern from
* test_commit_engine.c engine_setup_vcpus). The barrier reaches the engine
* via p->repl.ce (the same wiring the RETAIN hook path uses), so
* repl_set_commit_engine is part of the fixture. ACKs are driven through
* repl_on_ack (registry watermark + engine evaluation, the production
* path). current_lsn semantics: highest appended LSN = current_lsn - 1
* (persist.h invariant; persist.c:1021); current_lsn == 0 = nothing ever
* appended. n_peers=3 unless noted.
* ════════════════════════════════════════════════════════════════════════ */
/* Statics survive Unity's longjmp on assertion failure. */
static persist_t g_bp; /* stub persist for the barrier cases */
static commit_engine_t g_bce;
static int g_bep_a, g_bep_b; /* dummy endpoints (pointer compares) */
static void barrier_setup_vcpus(int n_peers, int n_standbys,
uint64_t current_lsn, int n_vcpus,
uint32_t sync_timeout_ms)
{
memset(&g_bp, 0, sizeof(g_bp));
TEST_ASSERT_EQUAL_INT(0, pthread_mutex_init(&g_bp.lock, NULL));
g_bp.config = persist_config_defaults();
g_bp.config.sync_timeout_ms = sync_timeout_ms;
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_bp.repl));
if (n_standbys >= 1)
TEST_ASSERT_TRUE(
repl_add_standby(&g_bp.repl, &g_bep_a, "bsb_a", 7300) >= 0);
if (n_standbys >= 2)
TEST_ASSERT_TRUE(
repl_add_standby(&g_bp.repl, &g_bep_b, "bsb_b", 7301) >= 0);
g_bp.current_lsn = current_lsn;
atomic_store(&g_bp.committed_lsn, 0);
TEST_ASSERT_EQUAL_INT(0,
commit_engine_init(&g_bce, &g_bp, n_peers, n_vcpus));
repl_set_commit_engine(&g_bp.repl, &g_bce);
}
static void barrier_setup(int n_peers, int n_standbys, uint64_t current_lsn)
{
barrier_setup_vcpus(n_peers, n_standbys, current_lsn, 8, 3000);
}
static void barrier_teardown(void)
{
commit_engine_fini(&g_bce);
repl_fini(&g_bp.repl);
pthread_mutex_destroy(&g_bp.lock);
}
static void cbr_sleep_ms(uint32_t ms)
{
struct timespec ts;
ts.tv_sec = ms / 1000u;
ts.tv_nsec = (long)(ms % 1000u) * 1000000L;
nanosleep(&ts, NULL);
}
/* Barrier-caller thread context (covering-ACK case). */
typedef struct {
uint32_t timeout_ms;
int rc;
uint64_t elapsed_ms;
} barrier_arg_t;
static barrier_arg_t g_barg;
static pthread_t g_btid;
static void *barrier_thread_fn(void *arg)
{
barrier_arg_t *a = arg;
uint64_t t0 = mono_ms();
a->rc = persist_commit_barrier(&g_bp, a->timeout_ms);
a->elapsed_ms = mono_ms() - t0;
return NULL;
}
/* Direct engine waiter — saturates the WAIT admission slot for the
* NOT_READY passthrough case (test_commit_engine.c test-13 pattern). */
typedef struct {
uint64_t lsn;
uint32_t timeout_ms;
int rc;
} engine_wait_arg_t;
static engine_wait_arg_t g_ewarg;
static pthread_t g_ewtid;
static void *engine_wait_thread_fn(void *arg)
{
engine_wait_arg_t *a = arg;
a->rc = commit_submit(&g_bce, a->lsn, COMMIT_MODE_WAIT, a->timeout_ms);
return NULL;
}
/* Poll until `want` waiters are blocked inside the engine or the deadline
* expires; returns the last observed inflight count. */
static int barrier_poll_inflight(int want, uint32_t deadline_ms)
{
uint64_t t0 = mono_ms();
int v;
for (;;) {
pthread_mutex_lock(&g_bce.lock);
v = g_bce.inflight_waits;
pthread_mutex_unlock(&g_bce.lock);
if (v >= want || mono_ms() - t0 >= deadline_ms)
return v;
cbr_sleep_ms(5);
}
}
/* ── Barrier short-circuits when already covered (Req 1.1) ──────────────── */
static void test_barrier_returns_zero_when_nothing_appended(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: 3-member cluster, NOTHING ever appended (current_lsn == 0,
* fresh persist) — there are no pending entries to wait for; ACKs
* withheld so any misrouted engine wait would block visibly */
barrier_setup(3, 2, 0);
/* Act */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 5000);
elapsed_ms = mono_ms() - t0;
/* Assert: immediate success without consuming the timeout */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"barrier over an empty log must return 0 (nothing to commit)");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
"barrier over an empty log must not block");
barrier_teardown();
printf("test_barrier_returns_zero_when_nothing_appended: PASS\n");
}
static void test_barrier_returns_zero_when_committed_covers_appended(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: entries 1..2 appended (current_lsn = 3); a standby watermark
* ACK has already committed both — committed_lsn covers the appended
* watermark current_lsn - 1 before the barrier is called */
barrier_setup(3, 2, 3);
repl_on_ack(&g_bp.repl, &g_bep_a, 2);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(2, atomic_load(&g_bp.committed_lsn),
"fixture: covering ACK must have committed the appended entries");
/* Act */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 5000);
elapsed_ms = mono_ms() - t0;
/* Assert: already covered → 0 immediately, no blocking */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"barrier must return 0 when committed_lsn already covers "
"current_lsn - 1");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
"already-covered barrier must not block");
barrier_teardown();
printf("test_barrier_returns_zero_when_committed_covers_appended: PASS\n");
}
/* ── Withheld ACKs → -ETIMEDOUT, committed_lsn unmoved (Req 1.2) ────────── */
static void test_barrier_times_out_when_acks_withheld(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: entry 1 pending (current_lsn = 2), ACKs withheld forever */
barrier_setup(3, 2, 2);
/* Act: short deadline, no ACK will ever arrive */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 250);
elapsed_ms = mono_ms() - t0;
/* Assert: timeout surfaced, deadline honored, commit index unmoved */
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
"withheld ACKs past the deadline must surface -ETIMEDOUT");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms >= 200,
"barrier must hold until its deadline before timing out");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, atomic_load(&g_bp.committed_lsn),
"committed_lsn must stay unmoved when no ACK ever covers the entry");
barrier_teardown();
printf("test_barrier_times_out_when_acks_withheld: PASS\n");
}
/* ── Covering ACK while blocked → 0 (Req 1.1) ───────────────────────────── */
static void test_barrier_returns_zero_once_covering_ack_arrives(void)
{
/* Arrange: entry 1 pending (current_lsn = 2); the barrier call runs in
* a thread so this thread can deliver the covering ACK */
barrier_setup(3, 2, 2);
g_barg.timeout_ms = 2000;
g_barg.rc = -9999;
g_barg.elapsed_ms = 0;
TEST_ASSERT_EQUAL_INT(
0, pthread_create(&g_btid, NULL, barrier_thread_fn, &g_barg));
/* Act: hold the barrier blocked >= 120 ms, then a covering watermark
* ACK advances the commit index; join BEFORE any assertion */
cbr_sleep_ms(120);
repl_on_ack(&g_bp.repl, &g_bep_a, 1);
pthread_join(g_btid, NULL);
/* Assert: success only after the ACK — the barrier really blocked */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_barg.rc,
"barrier must return 0 once a covering ACK advances the commit index");
TEST_ASSERT_TRUE_MESSAGE(g_barg.elapsed_ms >= 100,
"barrier must block until the covering ACK arrives, not before");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(1, atomic_load(&g_bp.committed_lsn),
"the covering ACK must have advanced committed_lsn over the entry");
barrier_teardown();
printf("test_barrier_returns_zero_once_covering_ack_arrives: PASS\n");
}
/* ── timeout_ms = 0 falls back to p->config.sync_timeout_ms (Req 1.2) ───── */
static void test_barrier_zero_timeout_falls_back_to_config_sync_timeout(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: entry 1 pending, ACKs withheld; config sync_timeout_ms set
* to a short, recognizable deadline (250 ms) */
barrier_setup_vcpus(3, 2, 2, 8, 250);
/* Act: explicit timeout 0 → must borrow the config deadline */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 0);
elapsed_ms = mono_ms() - t0;
/* Assert: timed out after ~the CONFIG deadline — neither an instant
* zero-timeout reject nor an unbounded wait */
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
"timeout_ms=0 with withheld ACKs must time out via the config value");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms >= 200,
"timeout_ms=0 must fall back to config.sync_timeout_ms, not 0");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1500,
"timeout_ms=0 must be bounded by config.sync_timeout_ms");
barrier_teardown();
printf("test_barrier_zero_timeout_falls_back_to_config_sync_timeout: "
"PASS\n");
}
/* ── n_peers=1 → immediate success (standalone preserved, Req 1.1) ──────── */
static void test_barrier_single_member_returns_immediately(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: single-member cluster, entry 1 pending (current_lsn = 2) —
* engine self-quorum: the primary's own append is the majority */
barrier_setup(1, 0, 2);
/* Act: large timeout that must NOT be consumed */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 5000);
elapsed_ms = mono_ms() - t0;
/* Assert: immediate local confirmation, standalone behavior preserved */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"n_peers=1 barrier must return 0 immediately (self-quorum)");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
"n_peers=1 barrier must not block");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(1, atomic_load(&g_bp.committed_lsn),
"self-quorum must advance committed_lsn over the entry");
barrier_teardown();
printf("test_barrier_single_member_returns_immediately: PASS\n");
}
/* ── Engine -XCE_ERR_NOT_READY passes through unmodified (Req 1.3) ────────
* n_vcpus=2, COMMIT_WAIT_RESERVED=2 → admission cap = max(1, 0) = 1
* (test_commit_engine.c test-13 pattern): one direct engine WAIT saturates
* the only slot, so the barrier's own commit_submit(WAIT) is rejected fast
* with the retriable admission error — which the barrier must return
* UNMODIFIED (no remapping, no success). */
static void test_barrier_not_ready_passes_through_unmodified(void)
{
int rc, observed_inflight;
uint64_t t0, elapsed_ms;
/* Arrange: entry 1 pending, cap = 1; a direct waiter occupies the slot */
barrier_setup_vcpus(3, 2, 2, 2, 3000);
g_ewarg.lsn = 1;
g_ewarg.timeout_ms = 5000;
g_ewarg.rc = -9999;
TEST_ASSERT_EQUAL_INT(
0, pthread_create(&g_ewtid, NULL, engine_wait_thread_fn, &g_ewarg));
observed_inflight = barrier_poll_inflight(1, 2000);
/* Act: barrier call while the admission cap is saturated */
t0 = mono_ms();
rc = persist_commit_barrier(&g_bp, 5000);
elapsed_ms = mono_ms() - t0;
/* Release the blocked waiter, then join BEFORE any assertion */
repl_on_ack(&g_bp.repl, &g_bep_a, 1);
pthread_join(g_ewtid, NULL);
/* Assert: the slot was really saturated when the barrier ran... */
TEST_ASSERT_EQUAL_INT_MESSAGE(1, observed_inflight,
"the direct engine waiter must occupy the only admission slot");
/* ...the engine's admission rejection passed through unmodified... */
TEST_ASSERT_EQUAL_INT_MESSAGE(-XCE_ERR_NOT_READY, rc,
"barrier must return the engine's -XCE_ERR_NOT_READY unmodified");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 500,
"admission-cap rejection must not block");
/* ...and the admitted waiter still completed via the covering ACK */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_ewarg.rc,
"the admitted direct waiter must complete with success");
barrier_teardown();
printf("test_barrier_not_ready_passes_through_unmodified: PASS\n");
}
/* ── Test group entry point ──────────────────────────────────────────────── */
void test_commit_barrier_rollout(void)
@ -422,5 +776,13 @@ void test_commit_barrier_rollout(void)
RUN_TEST(test_classify_is_total_over_wal_entry_type);
RUN_TEST(test_classify_unknown_type_fails_fast_in_testing);
RUN_TEST(test_fact_hook_append_is_retained_and_nonblocking);
/* Task 2.1 — persist_commit_barrier semantics (Req 1.1, 1.2, 1.3) */
RUN_TEST(test_barrier_returns_zero_when_nothing_appended);
RUN_TEST(test_barrier_returns_zero_when_committed_covers_appended);
RUN_TEST(test_barrier_times_out_when_acks_withheld);
RUN_TEST(test_barrier_returns_zero_once_covering_ack_arrives);
RUN_TEST(test_barrier_zero_timeout_falls_back_to_config_sync_timeout);
RUN_TEST(test_barrier_single_member_returns_immediately);
RUN_TEST(test_barrier_not_ready_passes_through_unmodified);
UNITY_END();
}

View file

@ -131,6 +131,21 @@ typedef enum {
* ERROR and returns WAL_MODE_RETAIN (never dropped, never confirmed early). */
wal_commit_mode_t wal_classify(uint32_t wal_type, int detail);
/* Commit barrier — handler-callable WAIT completion on everything appended
* so far (ha-commit-barrier-rollout). Snapshots the appended watermark
* (current_lsn - 1, under p->lock — matching persist_wal_sync_append's
* read); returns 0 without blocking when committed_lsn already covers it
* (or nothing was ever appended). Otherwise delegates to
* commit_submit(COMMIT_MODE_WAIT) and returns its code unmodified:
* 0 — committed on a majority
* -ETIMEDOUT — not committed within the deadline (entries remain
* in the log; outcome converges per umbrella 2.5)
* -XCE_ERR_NOT_READY — engine admission cap exhausted (retriable)
* timeout_ms = 0 falls back to p->config.sync_timeout_ms. n_peers <= 1
* returns 0 immediately (engine self-quorum, standalone preserved).
* Call only from vtask threads with no entity locks held. */
int persist_commit_barrier(persist_t* p, uint32_t timeout_ms);
/* Force WAL fdatasync (for tests and clean shutdown) */
int persist_flush(persist_t* p);