forked from lei.chen/myxce
feat(ha-commit-durability): gate legacy sync-append commit store on durable_lsn + commit<=durable invariant (task 2.4)
This commit is contained in:
parent
e4c4968331
commit
526bca9679
5 changed files with 250 additions and 4 deletions
|
|
@ -603,6 +603,178 @@ static void test_replicated_sync_failure_no_ack(void)
|
|||
printf("test_replicated_sync_failure_no_ack: PASS\n");
|
||||
}
|
||||
|
||||
/* ================================================================== *
|
||||
* task 2.4 — reconcile the dormant synchronous-append commit path *
|
||||
* *
|
||||
* persist_wal_sync_append's LEGACY branch (ce == NULL, or the LSN-0 *
|
||||
* first entry on the engine path) historically advanced committed_lsn *
|
||||
* DIRECTLY on a majority ACK (atomic_store committed_lsn = written_lsn) *
|
||||
* with NO durable precondition. Group-commit removed the premise that *
|
||||
* the append already fsync'd inline, so a majority ACK no longer *
|
||||
* implies the local entry is durable. This branch is the structural *
|
||||
* ACK-without-durability hazard task 2.4 must close: NO path may *
|
||||
* advance committed_lsn on a majority ACK unless the entry is durable *
|
||||
* locally (committed_lsn <= durable_lsn). *
|
||||
* *
|
||||
* NOTE: persist_wal_sync_append is NOT dormant — it is the LIVE *
|
||||
* job-submit path (persist_job_hook_fun, WAL_JOB_SUBMIT). So the fix is *
|
||||
* to ROUTE the legacy direct-store through the durable precondition, *
|
||||
* not to delete the function. *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/* Build an ENGINE-LESS persist with standbys registered, so
|
||||
* persist_wal_sync_append takes the legacy branch (ce == NULL) and its
|
||||
* has_standbys path: repl_wait_majority_ack returns 0 immediately for
|
||||
* the LSN-0 first entry, but for a NON-zero LSN it surfaces -EINVAL via
|
||||
* commit_submit (engine-less). The hazard we guard is the direct
|
||||
* committed_lsn store that the legacy branch performs on a 0 return. */
|
||||
|
||||
/* Drive the legacy branch directly: a single-member commit engine makes
|
||||
* the ENGINE path durable-safe, so to exercise the legacy direct-store
|
||||
* we use an engine-less persist and a registered standby that ACKs.
|
||||
* Rather than stand up the replication wire (out of boundary), we test
|
||||
* the invariant the fix establishes: committed_lsn must never exceed
|
||||
* durable_lsn after a commit advance, on EVERY path. */
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Test (RED before fix): the legacy direct-store path must NOT advance *
|
||||
* committed_lsn past durable_lsn. We drive the legacy branch via the *
|
||||
* LSN-0-style direct store by appending one entry that is NOT yet *
|
||||
* durable (slow cadence, no flush) and confirming committed_lsn stays *
|
||||
* at/below durable_lsn. *
|
||||
* *
|
||||
* Mechanism: persist_test_sync_append_legacy_store() is an XCE_TESTING *
|
||||
* seam that invokes the SAME guarded store the legacy branch performs *
|
||||
* (advance committed_lsn to written_lsn only if written_lsn <= *
|
||||
* durable_lsn). Pre-fix the store was unconditional, so this over- *
|
||||
* advanced committed past durable (RED). Post-fix it holds. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_legacy_sync_append_holds_committed_at_durable(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_legacycommit_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
/* Append one entry (LSN 1) but do NOT flush — durable_lsn lags at 0,
|
||||
* while the appended tip is 1. The legacy direct-store would advance
|
||||
* committed_lsn to the ACK'd written_lsn (1) — over-advancing past
|
||||
* the durable watermark (0). The durable precondition must hold it. */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
|
||||
uint64_t written_lsn = p->current_lsn - 1; /* 1 */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, written_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_durable_lsn(p));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, atomic_load(&p->committed_lsn));
|
||||
|
||||
/* Invoke the guarded legacy store directly (simulates a majority ACK
|
||||
* for an entry that is not yet durable locally). It must NOT advance
|
||||
* committed_lsn past durable_lsn. */
|
||||
persist_test_sync_append_legacy_commit_store(p, written_lsn);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
atomic_load(&p->committed_lsn) <= persist_durable_lsn(p),
|
||||
"legacy sync-append commit store must NOT advance committed_lsn past "
|
||||
"durable_lsn (ACK-without-durability hazard)");
|
||||
/* Concretely: committed stays at 0 because durable_lsn is still 0. */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, atomic_load(&p->committed_lsn));
|
||||
|
||||
/* Now make the entry durable and re-invoke: the store is now allowed
|
||||
* (written_lsn <= durable_lsn) and committed advances to it. */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
TEST_ASSERT_TRUE(persist_durable_lsn(p) >= written_lsn);
|
||||
persist_test_sync_append_legacy_commit_store(p, written_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(written_lsn, atomic_load(&p->committed_lsn));
|
||||
TEST_ASSERT_TRUE(atomic_load(&p->committed_lsn) <= persist_durable_lsn(p));
|
||||
|
||||
teardown_durability_persist(p);
|
||||
printf("test_legacy_sync_append_holds_committed_at_durable: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Test: the LSN-0 first-entry edge stays valid. durable_lsn and *
|
||||
* committed_lsn both start at 0; the legacy store of written_lsn == 0 *
|
||||
* (0 <= 0) is permitted and is a harmless no-op store of 0. This is the *
|
||||
* once-per-cluster-lifetime path the engine branch routes here. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_legacy_sync_append_lsn0_edge_valid(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_legacylsn0_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
/* Fresh store: durable_lsn == 0, committed_lsn == 0. */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_durable_lsn(p));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, atomic_load(&p->committed_lsn));
|
||||
|
||||
/* Storing written_lsn == 0 (0 <= durable 0) is allowed — a no-op. */
|
||||
persist_test_sync_append_legacy_commit_store(p, 0);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, atomic_load(&p->committed_lsn));
|
||||
TEST_ASSERT_TRUE(atomic_load(&p->committed_lsn) <= persist_durable_lsn(p));
|
||||
|
||||
teardown_durability_persist(p);
|
||||
printf("test_legacy_sync_append_lsn0_edge_valid: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Guard test (the Observable's "test guards against a future caller"): *
|
||||
* the committed_lsn <= durable_lsn invariant holds across the commit- *
|
||||
* engine advance path too. Drives the real engine + flusher and asserts *
|
||||
* that at every observed point committed never exceeds durable — the *
|
||||
* future-proof guard against reintroducing an ACK-without-durability *
|
||||
* commit on ANY path. In a debug build the in-code assert would abort *
|
||||
* if a path ever advanced committed past durable. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_commit_never_exceeds_durable_invariant(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_cinvariant_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
commit_engine_t ce;
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&ce, p, 1, 8));
|
||||
|
||||
/* Append a burst while sampling the invariant: committed must never
|
||||
* outrun durable, even mid-flight while the flusher advances both. */
|
||||
for (int round = 0; round < 6; round++) {
|
||||
for (int i = 0; i < 4; i++)
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, persist_wal_async(p, WAL_JOB_UPDATE, "q", 1));
|
||||
for (int s = 0; s < 50; s++) {
|
||||
uint64_t committed = atomic_load(&p->committed_lsn);
|
||||
uint64_t durable = persist_durable_lsn(p);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
committed <= durable,
|
||||
"committed_lsn must never exceed durable_lsn on any commit "
|
||||
"advance path");
|
||||
usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
/* And eventually committed catches up to the durable tip. */
|
||||
uint64_t tip = p->current_lsn - 1;
|
||||
uint64_t committed = 0;
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
committed = atomic_load(&p->committed_lsn);
|
||||
if (committed >= tip)
|
||||
break;
|
||||
usleep(1000);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT64(tip, committed);
|
||||
TEST_ASSERT_TRUE(atomic_load(&p->committed_lsn) <= persist_durable_lsn(p));
|
||||
|
||||
commit_engine_fini(&ce);
|
||||
teardown_durability_persist(p);
|
||||
printf("test_commit_never_exceeds_durable_invariant: PASS\n");
|
||||
}
|
||||
|
||||
#endif /* XCE_TESTING */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -622,6 +794,9 @@ void test_commit_durability(void)
|
|||
RUN_TEST(test_durable_advance_drives_commit_single_member);
|
||||
RUN_TEST(test_replicated_persist_syncs_before_return);
|
||||
RUN_TEST(test_replicated_sync_failure_no_ack);
|
||||
RUN_TEST(test_legacy_sync_append_holds_committed_at_durable);
|
||||
RUN_TEST(test_legacy_sync_append_lsn0_edge_valid);
|
||||
RUN_TEST(test_commit_never_exceeds_durable_invariant);
|
||||
#else
|
||||
printf("test_commit_durability: skipped (no XCE_TESTING)\n");
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue