diff --git a/.kiro/specs/ha-commit-barrier-rollout/tasks.md b/.kiro/specs/ha-commit-barrier-rollout/tasks.md index d1046b2c..f4e44601 100644 --- a/.kiro/specs/ha-commit-barrier-rollout/tasks.md +++ b/.kiro/specs/ha-commit-barrier-rollout/tasks.md @@ -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. diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md index 6d2c4dc4..5ad12991 100644 --- a/.wolf/cerebrum.md +++ b/.wolf/cerebrum.md @@ -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) diff --git a/.wolf/memory.md b/.wolf/memory.md index de339623..6ad5e905 100644 --- a/.wolf/memory.md +++ b/.wolf/memory.md @@ -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 | diff --git a/src/controller/persist.c b/src/controller/persist.c index 61042a12..035e25d8 100644 --- a/src/controller/persist.c +++ b/src/controller/persist.c @@ -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 diff --git a/src/controller/testsuite/test_commit_barrier_rollout.c b/src/controller/testsuite/test_commit_barrier_rollout.c index 9c6c4ebe..9d63a19e 100644 --- a/src/controller/testsuite/test_commit_barrier_rollout.c +++ b/src/controller/testsuite/test_commit_barrier_rollout.c @@ -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 #include +#include #include #include #include @@ -64,6 +78,7 @@ #include #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(); } diff --git a/src/include/persist.h b/src/include/persist.h index 14206230..252c1ad0 100644 --- a/src/include/persist.h +++ b/src/include/persist.h @@ -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);