forked from lei.chen/myxce
test(ha-commit-engine): RED tests for WAIT mode, admission cap, own-term gate (task 2.1)
9 new cases (8-16): block-until-majority, deadline timeout, standalone fast path, zero-ACK regression, n_vcpus-relative cap with floor, ACK progress at saturation, gate as blocking predicate (NOT_READY is admission-cap-only per corrected contract). Main-thread-only assertions after joins; 16 tests / 9 RED failures; baseline unchanged.
This commit is contained in:
parent
eae4767d7d
commit
d91f446e9e
4 changed files with 405 additions and 7 deletions
|
|
@ -18,7 +18,7 @@
|
|||
- _Requirements: 1.2, 2.1, 2.2, 2.3, 5.1_
|
||||
|
||||
- [ ] 2. WAIT mode, self-quorum, zero-peer rule, admission cap, own-term gate storage
|
||||
- [ ] 2.1 Write RED tests for blocking-wait semantics and admission control
|
||||
- [x] 2.1 Write RED tests for blocking-wait semantics and admission control
|
||||
- Tests in `test_commit_engine.c`: WAIT blocks until majority ACK then returns 0; WAIT returns `-ETIMEDOUT` at deadline with `committed_lsn` unmoved past the entry; n_peers=1 → WAIT returns 0 immediately and `committed_lsn` advances to the entry; n_peers=3 with zero standby ACKs → `-ETIMEDOUT`, never success (short-circuit regression); with n_vcpus=8 and reserved=2, a 7th concurrent WAIT gets `-XCE_ERR_NOT_READY` while the 6 admitted waits proceed; cap floor `max(1, n_vcpus - reserved)` holds for tiny n_vcpus; while the cap is saturated, `commit_on_ack` still advances `committed_lsn` and completes admitted waiters; `writable_lsn` unset (0) → no gating; `commit_set_writable_lsn(G)` with G > entry → waiter completes only once `committed_lsn >= G`
|
||||
- Observable completion: new cases FAIL (red) against the task-1 engine
|
||||
- _Requirements: 1.1, 4.3, 4.4, 6.3, 7.1, 7.2, 7.3_
|
||||
|
|
@ -85,3 +85,4 @@
|
|||
- (1.1) Full-suite output under default ASAN dies at exit via LeakSanitizer (pre-existing leaks in worker_join/rdhup_integration/start_election_async/liveness groups) without flushing stdout — use ASAN_OPTIONS=detect_leaks=0 for full-run logs or run groups standalone with -t.
|
||||
- (1.1) repl_add_standby returns the SLOT INDEX, not 0 (bug-410 note for fixture writers).
|
||||
- (1.1) commit.c is registered ONLY in controller_unit_test_src for now; production target linkage lands with task 5.3.
|
||||
- (2.1) WAIT/cap/gate tests (8-16) read `g_ce.inflight_waits` under `g_ce.lock` — task 2.2 must keep that field accurate while waiters block. Timing floors have ~20ms slack vs sleeps (reviewer advisory: watch for flake on loaded boxes). Unity assertions are main-thread-only after joins — keep that discipline in future threaded tests.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# anatomy.md
|
||||
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:36:56.598Z
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:56:17.297Z
|
||||
> Files: 511 tracked | Anatomy hits: 0 | Misses: 0
|
||||
|
||||
## ./
|
||||
|
|
@ -262,7 +262,7 @@
|
|||
- `requirements.md` — Requirements Document (~2715 tok)
|
||||
- `research.md` — Research Log: ha-commit-engine (~1345 tok)
|
||||
- `spec.json` (~140 tok)
|
||||
- `tasks.md` — Implementation Plan: ha-commit-engine (~2947 tok)
|
||||
- `tasks.md` — Implementation Plan: ha-commit-engine (~3031 tok)
|
||||
|
||||
## .kiro/specs/ha-config-bootstrap/
|
||||
|
||||
|
|
@ -716,4 +716,4 @@
|
|||
## src/controller/testsuite/
|
||||
|
||||
- `controller_test.c` — include <argp.h> (~1323 tok)
|
||||
- `test_commit_engine.c` — RED tests for per-LSN majority + commit-index integrity (Reqs 1.2, 2.1-2.3, 5.1): 7 cases against stub persist_t+replicator_t (~2459 tok)
|
||||
- `test_commit_engine.c` — Commit engine tests: tasks 1.x majority/commit-index (green) + task 2.1 WAIT blocking/admission-cap/own-term-gate RED cases (~13200 tok)
|
||||
|
|
|
|||
|
|
@ -873,3 +873,10 @@
|
|||
| 07:22 | Created src/controller/commit.c | — | ~2108 |
|
||||
| 2026-06-11 | ha-commit-engine task 1.2: implemented commit engine core (majority median, monotonic clamped committed_lsn, RETAIN) — 7/7 tests green | src/controller/commit.c | success | ~9k |
|
||||
| 07:36 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~14 |
|
||||
| 07:41 | Edited src/controller/testsuite/test_commit_engine.c | expanded (+17 lines) | ~339 |
|
||||
| 07:41 | Edited src/controller/testsuite/test_commit_engine.c | modified engine_setup_vcpus() | ~303 |
|
||||
| 07:43 | Edited src/controller/testsuite/test_commit_engine.c | modified struct() | ~3781 |
|
||||
| 07:43 | Edited src/controller/testsuite/test_commit_engine.c | 4→3 lines | ~17 |
|
||||
| 14:47 | task 2.1 RED tests: 9 WAIT/admission/gate cases added (16 total, 7 pass / 9 RED -ENOSYS) | src/controller/testsuite/test_commit_engine.c | RED verified, ASAN clean | ~9k |
|
||||
| 07:56 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~20 |
|
||||
| 07:56 | Edited .kiro/specs/ha-commit-engine/tasks.md | 1→2 lines | ~122 |
|
||||
|
|
|
|||
|
|
@ -17,9 +17,23 @@
|
|||
* These cases FAIL against the task-1.1 stub commit.c and go green with
|
||||
* the task-1.2 implementation.
|
||||
*
|
||||
* Requirements: 1.2, 2.1, 2.2, 2.3, 5.1
|
||||
* Task 2.1 extends the file with COMMIT_MODE_WAIT blocking-wait semantics,
|
||||
* admission control, and own-term-gate cases. They FAIL (RED) against the
|
||||
* task-1 engine (WAIT is -ENOSYS) and go green with task 2.2. Contract:
|
||||
* 0 — entry committed (majority ACK, gate satisfied)
|
||||
* -ETIMEDOUT — predicate unmet at deadline (incl. a gate that never
|
||||
* opens: the own-term gate is part of the WAIT
|
||||
* completion predicate, NOT an admission fast-reject)
|
||||
* -XCE_ERR_NOT_READY — admission cap exhausted (the ONLY cause);
|
||||
* cap = max(1, n_vcpus - COMMIT_WAIT_RESERVED)
|
||||
* Blocking waits run on pthreads; ACKs are fed from the main thread via the
|
||||
* registry + commit_on_ack. All threads are joined BEFORE any assertion so
|
||||
* Unity's longjmp never strands a live thread (ASAN/LSan clean).
|
||||
*
|
||||
* Requirements: 1.1, 1.2, 2.1, 2.2, 2.3, 4.3, 4.4, 5.1, 6.3, 7.1, 7.2, 7.3
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -27,6 +41,8 @@
|
|||
#include <time.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "xce/errnox.h"
|
||||
|
||||
#include "commit.h"
|
||||
#include "persist.h"
|
||||
#include "repl.h"
|
||||
|
|
@ -44,8 +60,10 @@ static int g_eps[MAX_STANDBYS];
|
|||
* Arrange a fresh engine over a stub persist_t + replicator_t.
|
||||
* appended_lsn = highest locally appended WAL entry; persist invariant is
|
||||
* committed_lsn <= current_lsn - 1, so current_lsn = appended_lsn + 1.
|
||||
* n_vcpus drives the WAIT admission cap: max(1, n_vcpus - COMMIT_WAIT_RESERVED).
|
||||
*/
|
||||
static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
|
||||
static void engine_setup_vcpus(int n_peers, int n_standbys, uint64_t appended_lsn,
|
||||
int n_vcpus)
|
||||
{
|
||||
int i;
|
||||
char name[16];
|
||||
|
|
@ -61,7 +79,12 @@ static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
|
|||
g_p.current_lsn = appended_lsn + 1;
|
||||
atomic_store(&g_p.committed_lsn, 0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, 8));
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, n_vcpus));
|
||||
}
|
||||
|
||||
static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
|
||||
{
|
||||
engine_setup_vcpus(n_peers, n_standbys, appended_lsn, 8);
|
||||
}
|
||||
|
||||
static void engine_teardown(void)
|
||||
|
|
@ -254,6 +277,363 @@ static void test_retain_mode_returns_zero_immediately(void)
|
|||
printf("test_retain_mode_returns_zero_immediately: PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Task 2.1 — COMMIT_MODE_WAIT blocking semantics, admission control,
|
||||
* own-term gate. RED against the task-1 engine (WAIT = -ENOSYS).
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Per-waiter pthread context. Static array: survives Unity's longjmp. */
|
||||
typedef struct {
|
||||
uint64_t lsn;
|
||||
uint32_t timeout_ms;
|
||||
int rc;
|
||||
uint64_t elapsed_ms;
|
||||
} wait_arg_t;
|
||||
|
||||
#define MAX_WAITERS 7
|
||||
|
||||
static wait_arg_t g_wargs[MAX_WAITERS];
|
||||
static pthread_t g_wtids[MAX_WAITERS];
|
||||
|
||||
/* Count of wait threads that have RETURNED from commit_submit — lets polls
|
||||
* exit fast in the RED phase, where WAIT returns -ENOSYS immediately. */
|
||||
static atomic_int g_waiters_done;
|
||||
|
||||
static void* wait_thread_fn(void* p)
|
||||
{
|
||||
wait_arg_t* a = (wait_arg_t*)p;
|
||||
uint64_t t0 = mono_ms();
|
||||
|
||||
a->rc = commit_submit(&g_ce, a->lsn, COMMIT_MODE_WAIT, a->timeout_ms);
|
||||
a->elapsed_ms = mono_ms() - t0;
|
||||
atomic_fetch_add(&g_waiters_done, 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void spawn_waiters(int n, uint64_t lsn, uint32_t timeout_ms)
|
||||
{
|
||||
int i;
|
||||
|
||||
atomic_store(&g_waiters_done, 0);
|
||||
for (i = 0; i < n; i++) {
|
||||
g_wargs[i].lsn = lsn;
|
||||
g_wargs[i].timeout_ms = timeout_ms;
|
||||
g_wargs[i].rc = -9999;
|
||||
g_wargs[i].elapsed_ms = 0;
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&g_wtids[i], NULL, wait_thread_fn, &g_wargs[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/* Join ALL spawned threads before any assertion: a failing TEST_ASSERT
|
||||
* longjmps out of the test, and a still-running waiter would then trip
|
||||
* ASAN/LSan and corrupt the static fixture for the next case. */
|
||||
static void join_waiters(int n)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
pthread_join(g_wtids[i], NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void sleep_ms(uint32_t ms)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
ts.tv_sec = ms / 1000u;
|
||||
ts.tv_nsec = (long)(ms % 1000u) * 1000000L;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
static int inflight_waits(void)
|
||||
{
|
||||
int v;
|
||||
|
||||
pthread_mutex_lock(&g_ce.lock);
|
||||
v = g_ce.inflight_waits;
|
||||
pthread_mutex_unlock(&g_ce.lock);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Poll until `want` waiters are blocked inside the engine, all `n_spawned`
|
||||
* waiters have already returned (RED fast path), or the deadline expires.
|
||||
* Returns the last observed inflight count. */
|
||||
static int poll_inflight(int want, int n_spawned, uint32_t deadline_ms)
|
||||
{
|
||||
uint64_t t0 = mono_ms();
|
||||
int v = inflight_waits();
|
||||
|
||||
while (v < want && atomic_load(&g_waiters_done) < n_spawned &&
|
||||
mono_ms() - t0 < deadline_ms) {
|
||||
sleep_ms(5);
|
||||
v = inflight_waits();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ── Test 8: WAIT blocks until majority ACK, then returns 0 ───────────────
|
||||
* (Req 1.1) */
|
||||
static void test_wait_blocks_until_majority_ack_then_returns_zero(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entry 1 appended, no ACKs yet */
|
||||
engine_setup(3, 2, 1);
|
||||
spawn_waiters(1, 1, 2000);
|
||||
|
||||
/* Act: hold the waiter for >= 120 ms, then a standby ACK covers it */
|
||||
sleep_ms(120);
|
||||
standby_ack(0, 1);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert: success only after the ACK — the wait really blocked */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_wargs[0].rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 100,
|
||||
"WAIT must block until the majority ACK arrives");
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_blocks_until_majority_ack_then_returns_zero: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 9: WAIT deadline → -ETIMEDOUT, committed_lsn unmoved past entry ─
|
||||
* (Req 2.1, 4.4) */
|
||||
static void test_wait_times_out_with_committed_lsn_unmoved_past_entry(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entries 1..2 appended; majority covers
|
||||
* only entry 1 — entry 2 can never commit within the wait */
|
||||
engine_setup(3, 2, 2);
|
||||
standby_ack(0, 1);
|
||||
|
||||
/* Act: WAIT on entry 2 with a short deadline; no further ACKs */
|
||||
spawn_waiters(1, 2, 250);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert: timeout surfaced, deadline honored, commit index never
|
||||
* advanced past the unacknowledged entry */
|
||||
TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 200,
|
||||
"WAIT must hold until its deadline");
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_times_out_with_committed_lsn_unmoved_past_entry: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 10: n_peers=1 → WAIT returns 0 immediately, committed advances ──
|
||||
* Standalone: self-ACK constitutes majority. (Req 6.3) */
|
||||
static void test_wait_single_member_returns_immediately_and_advances(void)
|
||||
{
|
||||
int rc;
|
||||
uint64_t t0, elapsed_ms;
|
||||
|
||||
/* Arrange: single-member cluster, entry 1 appended */
|
||||
engine_setup(1, 0, 1);
|
||||
|
||||
/* Act: WAIT with a large timeout that must NOT be consumed */
|
||||
t0 = mono_ms();
|
||||
rc = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000);
|
||||
elapsed_ms = mono_ms() - t0;
|
||||
|
||||
/* Assert: immediate local confirmation + committed_lsn advance */
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
|
||||
"single-member WAIT must not block");
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_single_member_returns_immediately_and_advances: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 11: n_peers=3, ZERO standby ACKs → -ETIMEDOUT, never success ────
|
||||
* Regression for the zero-active-standbys success short-circuit.
|
||||
* (Req 4.3, 4.4) */
|
||||
static void test_wait_zero_standby_acks_never_succeeds(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entry 1 appended, standbys never ACK */
|
||||
engine_setup(3, 2, 1);
|
||||
|
||||
/* Act */
|
||||
spawn_waiters(1, 1, 250);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert: failure result — zero peer ACKs are never a majority */
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].rc != 0,
|
||||
"zero standby ACKs must never be confirmed");
|
||||
TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_zero_standby_acks_never_succeeds: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 12: admission cap — 7th concurrent WAIT rejected, 6 admitted ────
|
||||
* n_vcpus=8, COMMIT_WAIT_RESERVED=2 → cap = 6. The 7th gets
|
||||
* -XCE_ERR_NOT_READY fast; the 6 admitted proceed; while the cap is
|
||||
* saturated, commit_on_ack still advances committed_lsn and completes the
|
||||
* admitted waiters. (Req 7.1, 7.2, 7.3) */
|
||||
static void test_wait_admission_cap_rejects_seventh_concurrent_wait(void)
|
||||
{
|
||||
int i, rc7, observed_inflight;
|
||||
uint64_t t0, elapsed7_ms;
|
||||
|
||||
/* Arrange: 3-member cluster, entry 1 appended, cap = max(1, 8-2) = 6 */
|
||||
engine_setup_vcpus(3, 2, 1, 8);
|
||||
spawn_waiters(6, 1, 5000);
|
||||
observed_inflight = poll_inflight(6, 6, 2000);
|
||||
|
||||
/* Act: 7th concurrent WAIT from this thread while 6 are blocked */
|
||||
t0 = mono_ms();
|
||||
rc7 = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000);
|
||||
elapsed7_ms = mono_ms() - t0;
|
||||
|
||||
/* ACK arrives while the cap is still saturated (the 6 are blocked) */
|
||||
standby_ack(0, 1);
|
||||
join_waiters(6);
|
||||
|
||||
/* Assert (after all joins): exactly 6 admitted... */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(6, observed_inflight,
|
||||
"all 6 admitted waits must block concurrently");
|
||||
/* ...7th rejected fast with the retriable admission error... */
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc7);
|
||||
TEST_ASSERT_TRUE_MESSAGE(elapsed7_ms < 500,
|
||||
"cap rejection must not block");
|
||||
/* ...ACK processing at saturation advanced committed_lsn (7.3)... */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
/* ...and completed every admitted waiter with success */
|
||||
for (i = 0; i < 6; i++) {
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_wargs[i].rc,
|
||||
"admitted waits must proceed to commit");
|
||||
}
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_admission_cap_rejects_seventh_concurrent_wait: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 13: cap floor max(1, n_vcpus - reserved) for tiny n_vcpus ───────
|
||||
* n_vcpus=2, reserved=2 → cap = max(1, 0) = 1: one wait is ALWAYS
|
||||
* admitted; the 2nd concurrent one is rejected. (Req 7.1, 7.2) */
|
||||
static void test_wait_admission_cap_floor_holds_for_tiny_vcpus(void)
|
||||
{
|
||||
int rc2;
|
||||
uint64_t t0, elapsed2_ms;
|
||||
|
||||
/* Arrange: 3-member cluster, entry 1 appended, cap = 1 */
|
||||
engine_setup_vcpus(3, 2, 1, 2);
|
||||
spawn_waiters(1, 1, 5000);
|
||||
(void)poll_inflight(1, 1, 2000);
|
||||
|
||||
/* Act: 2nd concurrent WAIT while the floor slot is occupied */
|
||||
t0 = mono_ms();
|
||||
rc2 = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000);
|
||||
elapsed2_ms = mono_ms() - t0;
|
||||
|
||||
standby_ack(0, 1);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert: the floor admits at least one waiter to completion... */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_wargs[0].rc,
|
||||
"cap floor must admit at least one wait");
|
||||
/* ...and the wait beyond the floor is rejected fast */
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc2);
|
||||
TEST_ASSERT_TRUE_MESSAGE(elapsed2_ms < 500, "cap rejection must not block");
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_admission_cap_floor_holds_for_tiny_vcpus: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 14: writable_lsn unset (0) → no gating ──────────────────────────
|
||||
* Gate defaults to 0 = no gate: WAIT on an already-committed entry returns
|
||||
* 0 immediately with no own-term condition applied. (Req 1.1) */
|
||||
static void test_wait_no_gate_when_writable_lsn_unset(void)
|
||||
{
|
||||
int rc;
|
||||
uint64_t t0, elapsed_ms;
|
||||
|
||||
/* Arrange: 3-member cluster, entry 1 already majority-committed;
|
||||
* commit_set_writable_lsn never called */
|
||||
engine_setup(3, 2, 1);
|
||||
standby_ack(0, 1);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
|
||||
/* Act */
|
||||
t0 = mono_ms();
|
||||
rc = commit_submit(&g_ce, 1, COMMIT_MODE_WAIT, 5000);
|
||||
elapsed_ms = mono_ms() - t0;
|
||||
|
||||
/* Assert: immediate success — no gate predicate when unset */
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
|
||||
"unset writable_lsn must not gate committed waits");
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_no_gate_when_writable_lsn_unset: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 15: gate G > entry → waiter completes only once committed >= G ──
|
||||
* The own-term gate is part of the WAIT completion predicate: the waiter
|
||||
* stays blocked after its OWN entry commits, completing only when
|
||||
* committed_lsn reaches the gate. (Req 1.1; design §Commit Engine) */
|
||||
static void test_wait_gate_blocks_until_committed_reaches_gate(void)
|
||||
{
|
||||
int done_after_entry_commit;
|
||||
|
||||
/* Arrange: 3-member cluster, entries 1..3 appended, gate G=3 > entry 1 */
|
||||
engine_setup(3, 2, 3);
|
||||
commit_set_writable_lsn(&g_ce, 3);
|
||||
spawn_waiters(1, 1, 3000);
|
||||
|
||||
/* Act: entry 1 gains majority — gate (3) still unmet */
|
||||
sleep_ms(50);
|
||||
standby_ack(0, 1);
|
||||
sleep_ms(150);
|
||||
done_after_entry_commit = atomic_load(&g_waiters_done);
|
||||
|
||||
/* Gate opens: committed_lsn reaches G */
|
||||
standby_ack(0, 3);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert (after join): waiter held while committed_lsn < G... */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, done_after_entry_commit,
|
||||
"waiter must not complete while committed_lsn < writable_lsn");
|
||||
/* ...then completed successfully once committed_lsn >= G */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_wargs[0].rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 180,
|
||||
"waiter must block past its own entry's commit");
|
||||
TEST_ASSERT_EQUAL_UINT64(3, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_gate_blocks_until_committed_reaches_gate: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 16: gate never opens → -ETIMEDOUT, NOT -XCE_ERR_NOT_READY ───────
|
||||
* CORRECTED contract: -XCE_ERR_NOT_READY is admission-cap-only. An unmet
|
||||
* gate blocks within the timeout and surfaces -ETIMEDOUT if it never
|
||||
* opens — never a NOT_READY fast-reject. (Req 1.1; design §Commit Engine) */
|
||||
static void test_wait_gate_never_opening_times_out_not_notready(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entry 1 committed, gate G=5 unreachable
|
||||
* (only entry 1 is appended) */
|
||||
engine_setup(3, 2, 1);
|
||||
commit_set_writable_lsn(&g_ce, 5);
|
||||
standby_ack(0, 1);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
|
||||
/* Act */
|
||||
spawn_waiters(1, 1, 250);
|
||||
join_waiters(1);
|
||||
|
||||
/* Assert: gate is a blocking predicate, not an admission fast-reject */
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].rc != -XCE_ERR_NOT_READY,
|
||||
"unmet gate must NOT fast-reject with NOT_READY");
|
||||
TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, g_wargs[0].rc);
|
||||
TEST_ASSERT_TRUE_MESSAGE(g_wargs[0].elapsed_ms >= 200,
|
||||
"gated WAIT must block until its deadline");
|
||||
|
||||
engine_teardown();
|
||||
printf("test_wait_gate_never_opening_times_out_not_notready: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test group entry point ──────────────────────────────────────────────── */
|
||||
|
||||
void test_commit_engine(void)
|
||||
|
|
@ -266,5 +646,15 @@ void test_commit_engine(void)
|
|||
RUN_TEST(test_committed_lsn_never_regresses);
|
||||
RUN_TEST(test_lagging_standby_does_not_block_majority_commit);
|
||||
RUN_TEST(test_retain_mode_returns_zero_immediately);
|
||||
/* Task 2.1 — WAIT semantics, admission control, own-term gate */
|
||||
RUN_TEST(test_wait_blocks_until_majority_ack_then_returns_zero);
|
||||
RUN_TEST(test_wait_times_out_with_committed_lsn_unmoved_past_entry);
|
||||
RUN_TEST(test_wait_single_member_returns_immediately_and_advances);
|
||||
RUN_TEST(test_wait_zero_standby_acks_never_succeeds);
|
||||
RUN_TEST(test_wait_admission_cap_rejects_seventh_concurrent_wait);
|
||||
RUN_TEST(test_wait_admission_cap_floor_holds_for_tiny_vcpus);
|
||||
RUN_TEST(test_wait_no_gate_when_writable_lsn_unset);
|
||||
RUN_TEST(test_wait_gate_blocks_until_committed_reaches_gate);
|
||||
RUN_TEST(test_wait_gate_never_opening_times_out_not_notready);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue