forked from lei.chen/myxce
feat(ha-commit-durability): cap commit self-contribution at durable_lsn + durable-advance commit wake (task 2.2)
This commit is contained in:
parent
cc83caf5a0
commit
c41edc515e
10 changed files with 395 additions and 13 deletions
|
|
@ -112,10 +112,37 @@ static void engine_setup_vcpus(int n_peers, int n_standbys, uint64_t appended_ls
|
|||
}
|
||||
g_p.current_lsn = appended_lsn + 1;
|
||||
atomic_store(&g_p.committed_lsn, 0);
|
||||
/* ha-commit-durability 2.2: the commit majority now caps self at
|
||||
* durable_lsn, not the appended tip. By default treat every appended
|
||||
* entry as already fdatasync'd locally (durable_lsn == appended tip), so
|
||||
* the pre-durability test semantics ("self covers every appended entry")
|
||||
* are preserved. Tests that exercise the self-cap lower durable_lsn
|
||||
* explicitly via set_durable() after setup. */
|
||||
atomic_store(&g_p.durable_lsn, appended_lsn);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, n_vcpus));
|
||||
}
|
||||
|
||||
/* Set the local durable watermark (highest LSN fdatasync'd locally). Mirrors
|
||||
* what the persist group-commit flusher does after a successful fdatasync;
|
||||
* here it is driven directly so the commit-engine self-cap can be tested in
|
||||
* isolation from real disk I/O. */
|
||||
static void set_durable(uint64_t durable_lsn)
|
||||
{
|
||||
atomic_store(&g_p.durable_lsn, durable_lsn);
|
||||
}
|
||||
|
||||
/* Drive the registered durable-advance callback exactly as the persist flusher
|
||||
* does after advancing durable_lsn and releasing p->lock: bump durable_lsn,
|
||||
* then invoke the hook so the commit engine re-evaluates + wakes WAIT callers.
|
||||
* Asserts a callback is actually registered (it is, from commit_engine_init). */
|
||||
static void advance_durable_and_wake(uint64_t durable_lsn)
|
||||
{
|
||||
atomic_store(&g_p.durable_lsn, durable_lsn);
|
||||
TEST_ASSERT_NOT_NULL(g_p.on_durable_advance);
|
||||
g_p.on_durable_advance(g_p.durable_cb_ctx);
|
||||
}
|
||||
|
||||
static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
|
||||
{
|
||||
engine_setup_vcpus(n_peers, n_standbys, appended_lsn, 8);
|
||||
|
|
@ -311,6 +338,104 @@ static void test_retain_mode_returns_zero_immediately(void)
|
|||
printf("test_retain_mode_returns_zero_immediately: PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* ha-commit-durability task 2.2 — self counts toward the commit majority only
|
||||
* when durable. The PRIMARY's self-contribution is capped at durable_lsn (the
|
||||
* highest LSN fdatasync'd locally), NOT the appended tip. (Req 1.2, 1.4)
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Test 2.2a: self-cap equals durable_lsn, not the appended tip ─────────
|
||||
* Appended tip is 5 but only LSN 3 is durable locally. A standby ACKs the
|
||||
* full tip (5). committed_lsn must stop at 3 — self only covers up to its
|
||||
* durable watermark, so entries 4,5 are NOT committed despite the standby
|
||||
* majority covering them. Kills the highest_appended_lsn self-cap. */
|
||||
static void test_self_cap_equals_durable_not_appended_tip(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entries 1..5 appended, but durable only to 3 */
|
||||
engine_setup(3, 2, 5);
|
||||
append_entries(5);
|
||||
set_durable(3);
|
||||
|
||||
/* Act: standby 0 ACKs the full appended tip (5) — self + sb0 majority
|
||||
* covers 5, but self is only durable to 3. */
|
||||
standby_ack(0, 5);
|
||||
|
||||
/* Assert: committed caps at the durable self-contribution (3), NOT 5. */
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
3, committed(),
|
||||
"self contribution must cap at durable_lsn, not the appended tip");
|
||||
|
||||
engine_teardown();
|
||||
printf("test_self_cap_equals_durable_not_appended_tip: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 2.2b: committed_lsn holds over an entry until it is durable ──────
|
||||
* The core observable (Req 1.2/1.4): with a full standby majority already
|
||||
* present, committed_lsn does NOT advance over entry 5 until entry 5 is
|
||||
* durable locally. As durable_lsn steps up, committed tracks it — never
|
||||
* passing the durable watermark. */
|
||||
static void test_committed_holds_until_durable_then_advances(void)
|
||||
{
|
||||
/* Arrange: 3-member cluster, entries 1..5 appended; nothing durable yet. */
|
||||
engine_setup(3, 2, 5);
|
||||
append_entries(5);
|
||||
set_durable(0);
|
||||
|
||||
/* A full standby majority covers the entire tip up front. */
|
||||
standby_ack(0, 5);
|
||||
standby_ack(1, 5);
|
||||
|
||||
/* Assert: with self durable to 0, NOTHING commits despite the majority. */
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
0, committed(),
|
||||
"no entry may commit while self is not durable for it");
|
||||
|
||||
/* Durable advances to 2 → committed may advance to 2 (self+majority). */
|
||||
set_durable(2);
|
||||
commit_on_ack(&g_ce, 0, 5); /* re-evaluate (an ACK re-fires evaluation) */
|
||||
TEST_ASSERT_EQUAL_UINT64(2, committed());
|
||||
|
||||
/* Durable advances to 5 → committed reaches the tip. */
|
||||
set_durable(5);
|
||||
commit_on_ack(&g_ce, 0, 5);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_committed_holds_until_durable_then_advances: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 2.2c: single-member self-cap — standalone still gated on durable ─
|
||||
* n_peers=1 self-quorum: committed advances on self alone, but ONLY up to
|
||||
* durable_lsn. An appended-but-unsynced entry is not committable even with
|
||||
* no peers. (Req 1.2, 6.3) */
|
||||
static void test_single_member_self_cap_gated_on_durable(void)
|
||||
{
|
||||
/* Arrange: single-member, entries 1..3 appended, durable only to 1. */
|
||||
engine_setup(1, 0, 3);
|
||||
append_entries(3);
|
||||
set_durable(1);
|
||||
|
||||
/* Act: drive an evaluation (membership change re-evaluates). */
|
||||
commit_on_membership_change(&g_ce);
|
||||
|
||||
/* Assert: self-quorum commits only the durable prefix (1), not the tip. */
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
1, committed(),
|
||||
"single-member commit must cap at durable_lsn");
|
||||
|
||||
/* Durable catches up → committed reaches the tip. */
|
||||
set_durable(3);
|
||||
commit_on_membership_change(&g_ce);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, committed());
|
||||
|
||||
engine_teardown();
|
||||
printf("test_single_member_self_cap_gated_on_durable: PASS\n");
|
||||
}
|
||||
|
||||
/* NOTE: test_durable_advance_wake_completes_blocked_wait (the liveness case
|
||||
* for the durable-advance wake) is defined AFTER the waiter-thread helpers
|
||||
* (spawn_waiters/poll_inflight/join_waiters), further down this file. */
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Task 2.1 — COMMIT_MODE_WAIT blocking semantics, admission control,
|
||||
* own-term gate. RED against the task-1 engine (WAIT = -ENOSYS).
|
||||
|
|
@ -406,6 +531,53 @@ static int poll_inflight(int want, int n_spawned, uint32_t deadline_ms)
|
|||
return v;
|
||||
}
|
||||
|
||||
/* ── Test 2.2d: durable-advance wake completes a blocked WAIT promptly ─────
|
||||
* Liveness for change (B): a COMMIT_MODE_WAIT caller whose standby majority
|
||||
* already covers its entry, but whose entry is not yet durable locally, must
|
||||
* complete PROMPTLY when the flusher advances durable_lsn and fires the
|
||||
* durable-advance callback — NOT only when its timeout expires. The standby
|
||||
* ACK arrives BEFORE the durable advance, so commit_on_ack alone cannot
|
||||
* complete it; the durable-advance wake must. (Req 1.2, 1.4) */
|
||||
static void test_durable_advance_wake_completes_blocked_wait(void)
|
||||
{
|
||||
uint64_t t0, elapsed_ms;
|
||||
|
||||
/* Arrange: 3-member cluster, entry 1 appended, NOT durable yet. */
|
||||
engine_setup(3, 2, 1);
|
||||
append_entries(1);
|
||||
set_durable(0);
|
||||
|
||||
/* Full standby majority ACKs entry 1 up front — but self is not durable,
|
||||
* so this ACK does NOT commit it (committed holds at 0). */
|
||||
standby_ack(0, 1);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, committed());
|
||||
|
||||
/* A WAIT caller blocks on entry 1 with a long timeout (5s). It cannot
|
||||
* complete on the ACK (already delivered) — only the durable-advance wake
|
||||
* can release it before the timeout. */
|
||||
spawn_waiters(1, 1, 5000);
|
||||
(void)poll_inflight(1, 1, 2000); /* ensure the waiter is actually blocked */
|
||||
|
||||
/* Act: the "flusher" advances durable_lsn to 1 and fires the wake. */
|
||||
t0 = mono_ms();
|
||||
advance_durable_and_wake(1);
|
||||
join_waiters(1);
|
||||
elapsed_ms = mono_ms() - t0;
|
||||
|
||||
/* Assert: the waiter completed with success, PROMPTLY (well under the 5s
|
||||
* timeout) — proving the durable-advance wake, not the deadline. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, g_wargs[0].rc,
|
||||
"WAIT must complete on durable-advance, not time out");
|
||||
TEST_ASSERT_EQUAL_UINT64(1, committed());
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
elapsed_ms < 2000,
|
||||
"durable-advance wake must complete the wait promptly, not on timeout");
|
||||
|
||||
engine_teardown();
|
||||
printf("test_durable_advance_wake_completes_blocked_wait: PASS\n");
|
||||
}
|
||||
|
||||
/* ── 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)
|
||||
|
|
@ -1421,6 +1593,11 @@ 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);
|
||||
/* ha-commit-durability task 2.2 — self counts toward majority only when durable */
|
||||
RUN_TEST(test_self_cap_equals_durable_not_appended_tip);
|
||||
RUN_TEST(test_committed_holds_until_durable_then_advances);
|
||||
RUN_TEST(test_single_member_self_cap_gated_on_durable);
|
||||
RUN_TEST(test_durable_advance_wake_completes_blocked_wait);
|
||||
/* 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue