#define _GNU_SOURCE #include #include #include #include #include #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 }