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:
parent
e8ecc87551
commit
611fbc8d7b
6 changed files with 405 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue