commit_submit_async() lets a WAIT-mode caller register a continuation against an appended LSN instead of blocking on pthread_cond_timedwait, reusing the existing admission-cap discipline (wait_admit_locked/ wait_release_locked) so the bounded-capacity guarantee carries over unchanged. Purely additive: no existing call site (repl_wait_majority_ack) is touched yet -- that switch lands in task 3.2 once commit_resolve() (task 1.2) exists to drive it.
311 lines
12 KiB
C
311 lines
12 KiB
C
/*
|
|
* 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 <errno.h>
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#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);
|
|
}
|