feat(ha-commit-durability): add XCE_TESTING durable-LSN-on-disk verification seam (task 1.2)
This commit is contained in:
parent
1ca2d640cb
commit
f90395282c
6 changed files with 357 additions and 1 deletions
|
|
@ -59,6 +59,7 @@ extern void test_egress_filter(void);
|
|||
extern void test_demotion_convergence(void);
|
||||
extern void test_convergence_verification(void);
|
||||
extern void test_coldstart_lsn0(void);
|
||||
extern void test_commit_durability(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -187,6 +188,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_demotion_convergence);
|
||||
TEST(test_convergence_verification);
|
||||
TEST(test_coldstart_lsn0);
|
||||
TEST(test_commit_durability);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
189
src/controller/testsuite/test_commit_durability.c
Normal file
189
src/controller/testsuite/test_commit_durability.c
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "persist.h"
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* ha-commit-durability — task 1.2: durability-verification test seam *
|
||||
* *
|
||||
* These tests exercise persist_test_durable_lsn_on_disk(), the *
|
||||
* XCE_TESTING-only seam that re-reads + CRC-validates the WAL from *
|
||||
* stable storage and reports the highest fully-persisted LSN — *
|
||||
* independent of the in-memory current_lsn watermark. This lets a *
|
||||
* test assert an acknowledged operation is recoverable from disk after *
|
||||
* an immediate ungraceful termination, without relying on the *
|
||||
* background flush timer (Req 3.2). The seam is purely inspective: *
|
||||
* it changes nothing observable about the acknowledgment contract *
|
||||
* (Req 3.3). *
|
||||
* *
|
||||
* The seam is compiled ONLY into the controller_unit_test target *
|
||||
* (-DXCE_TESTING). The production xce_controller target links without *
|
||||
* it — see #ifdef XCE_TESTING in persist.c / declaration in persist.h. *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef XCE_TESTING
|
||||
|
||||
/* Build a persist instance with a deliberately slow background flush
|
||||
* cadence, so the only durability point is an explicit persist_flush()
|
||||
* (mirrors make_fresh_persist in test_persist_recovery.c). */
|
||||
static persist_t* make_durability_persist(const char* tmpdir)
|
||||
{
|
||||
persist_t* p = calloc(1, sizeof(persist_t));
|
||||
if (!p)
|
||||
return NULL;
|
||||
|
||||
persist_config_t cfg = persist_config_defaults();
|
||||
cfg.wal_sync_ms = 5000; /* slow background fsync — durability comes from
|
||||
* the explicit flush, not the timer */
|
||||
cfg.snap_keep_n = 2;
|
||||
|
||||
persist_set_workdir(p, tmpdir, "ctrl-test");
|
||||
|
||||
if (persist_init(p, &cfg, NULL) != 0) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
if (persist_start(p) != 0) {
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static void teardown_durability_persist(persist_t* p)
|
||||
{
|
||||
persist_stop(p);
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Test 1: an empty WAL has no durable entries (floor == 0). *
|
||||
* An empty first-boot WAL is not corruption — it simply has nothing *
|
||||
* persisted, so the seam reports 0. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_durable_floor_empty(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_durfloor0_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
uint64_t floor = persist_test_durable_lsn_on_disk(p);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, floor);
|
||||
|
||||
teardown_durability_persist(p);
|
||||
printf("test_durable_floor_empty: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Test 2 (core Req 3.2): after append + explicit fdatasync, the seam *
|
||||
* sees the entries on disk — the durable floor equals the highest *
|
||||
* appended LSN, recoverable independent of in-memory state. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_durable_floor_after_flush(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_durfloor1_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
/* Append three entries. LSN 0 is reserved, so these take LSN 1,2,3.
|
||||
* current_lsn advances to 4 (one past the last appended). */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "b", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "c", 1));
|
||||
|
||||
/* Durability point: fdatasync the segment so the bytes are on stable
|
||||
* storage (this is what an acknowledged-before-durable contract must
|
||||
* have done before replying ok). */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
|
||||
/* The seam re-reads from disk and reports the highest intact LSN. */
|
||||
uint64_t floor = persist_test_durable_lsn_on_disk(p);
|
||||
TEST_ASSERT_EQUAL_UINT64(p->current_lsn - 1, floor);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, floor);
|
||||
|
||||
teardown_durability_persist(p);
|
||||
printf("test_durable_floor_after_flush: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Test 3 (recoverable-after-ungraceful-kill, Req 3.2): a SECOND *
|
||||
* persist instance opened over the same WAL dir — with NO in-memory *
|
||||
* state from the writer — recovers exactly the durable floor the seam *
|
||||
* reported. This is the "survives an immediate ungraceful *
|
||||
* termination" assertion: durability lives on disk, not in the *
|
||||
* volatile buffers of the process that wrote it. *
|
||||
* ------------------------------------------------------------------ */
|
||||
static void test_durable_floor_survives_process_loss(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_durfloor2_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
uint64_t durable_floor = 0;
|
||||
|
||||
/* Writer process: append, fdatasync, then "die" (we drop the instance
|
||||
* without any further flush — the bytes are already durable). */
|
||||
{
|
||||
persist_t* p = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "x", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "y", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
|
||||
durable_floor = persist_test_durable_lsn_on_disk(p);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, durable_floor);
|
||||
|
||||
teardown_durability_persist(p);
|
||||
}
|
||||
|
||||
/* "Restarted" process: a fresh persist_t with zero in-memory state,
|
||||
* opened over the same on-disk WAL. Its durable floor — read straight
|
||||
* from disk before any recover/replay — matches what the writer
|
||||
* persisted. Acknowledged-before-durable ops therefore survive the
|
||||
* loss of the acking process. */
|
||||
{
|
||||
persist_t* p2 = make_durability_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p2);
|
||||
|
||||
/* current_lsn is still the fresh-boot value (1, LSN 0 reserved):
|
||||
* the durable floor does NOT come from in-memory state. */
|
||||
uint64_t floor2 = persist_test_durable_lsn_on_disk(p2);
|
||||
TEST_ASSERT_EQUAL_UINT64(durable_floor, floor2);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, floor2);
|
||||
|
||||
teardown_durability_persist(p2);
|
||||
}
|
||||
|
||||
printf("test_durable_floor_survives_process_loss: PASS\n");
|
||||
}
|
||||
|
||||
#endif /* XCE_TESTING */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test suite entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
void test_commit_durability(void)
|
||||
{
|
||||
#ifdef XCE_TESTING
|
||||
RUN_TEST(test_durable_floor_empty);
|
||||
RUN_TEST(test_durable_floor_after_flush);
|
||||
RUN_TEST(test_durable_floor_survives_process_loss);
|
||||
#else
|
||||
printf("test_commit_durability: skipped (no XCE_TESTING)\n");
|
||||
#endif
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue