/* * test_commit_resolve.c — raft-apply-decoupling task 1.1. * * Keystone conversion, registration half: the commit engine gains * commit_submit_async(), a non-blocking sibling of * commit_submit(..., COMMIT_MODE_WAIT, ...) that registers a continuation * (callback + context) against an appended LSN instead of parking the * calling thread. This file covers ONLY the registration mechanism — * resolution (invoking the continuation on majority-ack / timeout / * demotion / truncation) is wired by a follow-up task (1.2) and is not * exercised here: a registered continuation is never invoked in these * tests, by design. * * Asserts: * (1) commit_submit_async returns immediately (0), before the LSN * commits, and does NOT invoke the continuation inline. * (2) Bad-argument rejection: -EINVAL for NULL engine, LSN 0, NULL fn. * (3) The SAME admission-cap discipline commit_submit's WAIT mode uses * (wait_admit_locked / wait_cap / inflight_waits) governs * registration: cap exhaustion fast-rejects with -XCE_ERR_NOT_READY, * never blocks. * (4) That admission budget is SHARED with COMMIT_MODE_WAIT — a parked * blocking waiter and a pending async registration draw from the * same inflight_waits pool. * (5) Distinct concurrent registrations occupy distinct per-LSN slots. * * Requirements: 1.3, 1.4, 9.1, 9.2 */ #include #include #include #include #include #include #include #include #include "xce/errnox.h" #include "commit.h" #include "repl.h" /* ── Fixture ────────────────────────────────────────────────────────────── * Static: persist_t embeds large path buffers, and statics survive Unity's * longjmp on assertion failure (pattern from test_commit_engine.c / * test_commit_admission_pool.c). */ static persist_t g_p; static commit_engine_t g_ce; static int g_eps[MAX_STANDBYS]; /* dummy endpoints — pointer-only */ /* Arrange a fresh engine over a stub persist_t + replicator_t, mirroring * test_commit_engine.c's engine_setup_vcpus. n_vcpus drives the local * admission cap: max(1, n_vcpus - COMMIT_WAIT_RESERVED). */ static void resolve_setup(int n_peers, int n_standbys, uint64_t appended_lsn, int n_vcpus) { char name[16]; memset(&g_p, 0, sizeof(g_p)); TEST_ASSERT_EQUAL_INT(0, repl_init(&g_p.repl)); for (int i = 0; i < n_standbys; i++) { snprintf(name, sizeof(name), "sb%d", i); TEST_ASSERT_EQUAL_INT( i, repl_add_standby(&g_p.repl, &g_eps[i], name, 7000 + (uint32_t)i)); } g_p.current_lsn = appended_lsn + 1; atomic_store(&g_p.committed_lsn, 0); atomic_store(&g_p.durable_lsn, appended_lsn); TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, n_vcpus)); } static void resolve_teardown(void) { commit_engine_fini(&g_ce); repl_fini(&g_p.repl); } /* Continuation recorder — proves (non-)invocation and captures the args a * future commit_resolve() would deliver. Not exercised as a real resolve * path in this file (task 1.2's job); only used to assert it stays quiet. */ typedef struct { _Atomic int calls; uint64_t last_lsn; commit_outcome_t last_outcome; int last_rc; void* last_ctx; } continuation_recorder_t; static void recorder_fn(void* ctx, uint64_t lsn, commit_outcome_t outcome, int rc) { continuation_recorder_t* r = (continuation_recorder_t*)ctx; r->last_lsn = lsn; r->last_outcome = outcome; r->last_rc = rc; r->last_ctx = ctx; atomic_fetch_add(&r->calls, 1); } /* ------------------------------------------------------------------ * * (1) Registration returns immediately, before the LSN commits, and * * never invokes the continuation inline. * * ------------------------------------------------------------------ */ static void test_commit_submit_async_returns_immediately_before_commit(void) { continuation_recorder_t rec = {0}; /* n_peers=3, no standby ACKs delivered: LSN 1 can never commit on its * own (majority unreachable), so a blocking WAIT would park here. */ resolve_setup(/*n_peers=*/3, /*n_standbys=*/2, /*appended_lsn=*/1, /*n_vcpus=*/8); int rc = commit_submit_async(&g_ce, /*lsn=*/1, recorder_fn, &rec); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, rc, "registration must succeed and return without blocking"); TEST_ASSERT_EQUAL_UINT64_MESSAGE( 0, atomic_load(&g_p.committed_lsn), "the LSN must still be uncommitted at the point of registration"); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, atomic_load(&rec.calls), "the continuation must NOT be invoked by registration alone"); resolve_teardown(); printf("test_commit_submit_async_returns_immediately_before_commit: " "PASS\n"); } /* ------------------------------------------------------------------ * * (2) Bad-argument rejection. * * ------------------------------------------------------------------ */ static void test_commit_submit_async_rejects_invalid_args(void) { continuation_recorder_t rec = {0}; resolve_setup(/*n_peers=*/1, /*n_standbys=*/0, /*appended_lsn=*/1, /*n_vcpus=*/8); TEST_ASSERT_EQUAL_INT(-EINVAL, commit_submit_async(NULL, 1, recorder_fn, &rec)); TEST_ASSERT_EQUAL_INT( -EINVAL, commit_submit_async(&g_ce, /*lsn=*/0, recorder_fn, &rec)); TEST_ASSERT_EQUAL_INT(-EINVAL, commit_submit_async(&g_ce, /*lsn=*/1, NULL, &rec)); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, atomic_load(&rec.calls), "no rejected registration may invoke the continuation"); resolve_teardown(); printf("test_commit_submit_async_rejects_invalid_args: PASS\n"); } /* ------------------------------------------------------------------ * * (3) Admission cap governs registration exactly as it governs WAIT: * * n_vcpus=2, COMMIT_WAIT_RESERVED=2 -> local cap = max(1,0) = 1. * * ------------------------------------------------------------------ */ static void test_commit_submit_async_respects_admission_cap(void) { continuation_recorder_t rec1 = {0}, rec2 = {0}; resolve_setup(/*n_peers=*/3, /*n_standbys=*/2, /*appended_lsn=*/2, /*n_vcpus=*/2); TEST_ASSERT_EQUAL_INT(0, commit_submit_async(&g_ce, 1, recorder_fn, &rec1)); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_ce.inflight_waits, "registration consumes an admission slot"); int rc = commit_submit_async(&g_ce, 2, recorder_fn, &rec2); TEST_ASSERT_EQUAL_INT_MESSAGE( -XCE_ERR_NOT_READY, rc, "over-cap registration must fast-reject, never block"); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, g_ce.inflight_waits, "a rejected registration must not consume a slot"); TEST_ASSERT_EQUAL_INT(0, atomic_load(&rec1.calls)); TEST_ASSERT_EQUAL_INT(0, atomic_load(&rec2.calls)); resolve_teardown(); printf("test_commit_submit_async_respects_admission_cap: PASS\n"); } /* ------------------------------------------------------------------ * * (4) The admission budget is SHARED with COMMIT_MODE_WAIT: a parked * * blocking waiter occupies the same pool an async registration * * draws from, and releasing it frees the slot for either caller. * * ------------------------------------------------------------------ */ typedef struct { uint64_t lsn; uint32_t timeout_ms; int rc; } waiter_arg_t; static void* waiter_thread(void* a) { waiter_arg_t* w = (waiter_arg_t*)a; w->rc = commit_submit(&g_ce, w->lsn, COMMIT_MODE_WAIT, w->timeout_ms); return NULL; } static uint64_t mono_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } /* Poll ce->inflight_waits until it reaches `want` or the deadline elapses; * returns the last observed value (pattern from test_commit_admission_pool.c * poll_inflight — avoids a fixed sleep race against the background thread's * admission acquire). */ static int poll_inflight(int want, uint32_t deadline_ms) { uint64_t t0 = mono_ms(); int v; for (;;) { pthread_mutex_lock(&g_ce.lock); v = g_ce.inflight_waits; pthread_mutex_unlock(&g_ce.lock); if (v >= want || mono_ms() - t0 >= deadline_ms) { return v; } usleep(500); } } static void test_commit_submit_async_shares_admission_budget_with_wait(void) { continuation_recorder_t rec = {0}; pthread_t tid; waiter_arg_t w = {.lsn = 1, .timeout_ms = 4000, .rc = -9999}; /* n_vcpus=2 -> local cap = 1 (same sizing as test 3 above). */ resolve_setup(/*n_peers=*/3, /*n_standbys=*/2, /*appended_lsn=*/2, /*n_vcpus=*/2); TEST_ASSERT_EQUAL_INT(0, pthread_create(&tid, NULL, waiter_thread, &w)); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, poll_inflight(1, 3000), "the blocking WAIT must occupy the sole admission slot"); int rc = commit_submit_async(&g_ce, /*lsn=*/2, recorder_fn, &rec); TEST_ASSERT_EQUAL_INT_MESSAGE( -XCE_ERR_NOT_READY, rc, "async registration must fast-reject while a blocking WAIT holds " "the shared admission slot"); /* Release the blocking waiter via a covering majority ACK (self + 1 * standby = 2 > 3/2), freeing the slot for the pending registration. */ repl_on_ack(&g_p.repl, &g_eps[0], 1); commit_on_ack(&g_ce, 0, 1); TEST_ASSERT_EQUAL_INT(0, pthread_join(tid, NULL)); TEST_ASSERT_EQUAL_INT(0, w.rc); rc = commit_submit_async(&g_ce, /*lsn=*/2, recorder_fn, &rec); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, rc, "registration must succeed once the shared slot is freed"); resolve_teardown(); printf("test_commit_submit_async_shares_admission_budget_with_wait: " "PASS\n"); } /* ------------------------------------------------------------------ * * (5) Distinct concurrent registrations occupy distinct per-LSN slots. * * ------------------------------------------------------------------ */ static void test_commit_submit_async_tracks_distinct_lsns(void) { continuation_recorder_t rec1 = {0}, rec2 = {0}; resolve_setup(/*n_peers=*/3, /*n_standbys=*/2, /*appended_lsn=*/2, /*n_vcpus=*/8); TEST_ASSERT_EQUAL_INT(0, commit_submit_async(&g_ce, 1, recorder_fn, &rec1)); TEST_ASSERT_EQUAL_INT(0, commit_submit_async(&g_ce, 2, recorder_fn, &rec2)); TEST_ASSERT_EQUAL_INT(2, g_ce.inflight_waits); /* White-box: the two registrations must land in distinct slots, each * carrying its own lsn/fn/ctx (the per-LSN bookkeeping the task * requires, not a single-shot overwrite). */ int found_lsn1 = 0, found_lsn2 = 0; for (int i = 0; i < g_ce.n_continuations; i++) { if (g_ce.continuations[i].lsn == 1) { found_lsn1 = 1; TEST_ASSERT_EQUAL_PTR(recorder_fn, g_ce.continuations[i].fn); TEST_ASSERT_EQUAL_PTR(&rec1, g_ce.continuations[i].ctx); } else if (g_ce.continuations[i].lsn == 2) { found_lsn2 = 1; TEST_ASSERT_EQUAL_PTR(recorder_fn, g_ce.continuations[i].fn); TEST_ASSERT_EQUAL_PTR(&rec2, g_ce.continuations[i].ctx); } } TEST_ASSERT_TRUE_MESSAGE(found_lsn1, "lsn=1 registration must be tracked"); TEST_ASSERT_TRUE_MESSAGE(found_lsn2, "lsn=2 registration must be tracked"); resolve_teardown(); printf("test_commit_submit_async_tracks_distinct_lsns: PASS\n"); } void test_commit_resolve(void) { RUN_TEST(test_commit_submit_async_returns_immediately_before_commit); RUN_TEST(test_commit_submit_async_rejects_invalid_args); RUN_TEST(test_commit_submit_async_respects_admission_cap); RUN_TEST(test_commit_submit_async_shares_admission_budget_with_wait); RUN_TEST(test_commit_submit_async_tracks_distinct_lsns); }