/* P0-15b: Checkpoint E2E — round trip * * Exercises cross-phase interactions: * - WAL writer (P0-B) + checkpoint (P0-C) + recovery (P0-C) * - LSN continuity across checkpoint + post-checkpoint WAL entries * - WAL segment trimming after checkpoint * - Snapshot rotation keeping snap_keep_n files */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "persist.h" /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static int count_files_suffix(const char* dir, const char* suffix) { DIR* d = opendir(dir); if (!d) return 0; int n = 0; size_t slen = strlen(suffix); struct dirent* e; while ((e = readdir(d)) != NULL) { size_t len = strlen(e->d_name); if (len > slen && strcmp(e->d_name + len - slen, suffix) == 0) n++; } closedir(d); return n; } /* Make a persist_t with a fresh tmpdir. Caller must persist_stop/fini/free. */ static persist_t* make_ckpt_e2e(char* tmpdir, size_t sz, uint32_t snap_keep_n) { snprintf(tmpdir, sz, "/tmp/xce_e2e_ckpt_XXXXXX"); if (!mkdtemp(tmpdir)) return NULL; persist_t* p = calloc(1, sizeof(persist_t)); if (!p) return NULL; persist_config_t cfg = persist_config_defaults(); cfg.wal_sync_ms = 5000; cfg.snap_keep_n = snap_keep_n; persist_set_workdir(p, tmpdir, "e2e-ckpt"); 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_ckpt_e2e(persist_t* p) { persist_stop(p); persist_fini(p); free(p); } /* ------------------------------------------------------------------ */ /* Test 1: LSN continuity — 20 WAL + checkpoint + 10 WAL + recovery */ /* ------------------------------------------------------------------ */ static void test_checkpoint_recovery_lsn_continuity(void) { char tmpdir[256]; persist_t* p = make_ckpt_e2e(tmpdir, sizeof(tmpdir), 2); TEST_ASSERT_NOT_NULL(p); /* Phase 1: 20 WAL entries */ for (int i = 0; i < 20; i++) { TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "x", 1)); } /* Checkpoint */ TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p)); uint64_t ckpt_lsn = p->checkpoint_lsn; TEST_ASSERT_TRUE(ckpt_lsn > 0); /* Phase 2: 10 more WAL entries after checkpoint */ for (int i = 0; i < 10; i++) { TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "y", 1)); } TEST_ASSERT_EQUAL_INT(0, persist_flush(p)); teardown_ckpt_e2e(p); /* Phase 3: fresh persist_t, recover */ persist_t* p2 = calloc(1, sizeof(persist_t)); TEST_ASSERT_NOT_NULL(p2); persist_config_t cfg = persist_config_defaults(); cfg.wal_sync_ms = 5000; cfg.snap_keep_n = 2; persist_set_workdir(p2, tmpdir, "e2e-ckpt"); TEST_ASSERT_EQUAL_INT(0, persist_init(p2, &cfg, NULL)); TEST_ASSERT_EQUAL_INT(0, persist_start(p2)); TEST_ASSERT_EQUAL_INT(0, persist_recover(p2)); /* All 30+ entries must be accounted for */ TEST_ASSERT_TRUE(p2->current_lsn >= 30); /* The snapshot was used */ TEST_ASSERT_TRUE(p2->checkpoint_lsn > 0); teardown_ckpt_e2e(p2); printf("test_checkpoint_recovery_lsn_continuity: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 2: WAL trim — old segments removed after two checkpoints */ /* ------------------------------------------------------------------ */ static void test_checkpoint_wal_trim_after_checkpoint(void) { char tmpdir[256]; persist_t* p = make_ckpt_e2e(tmpdir, sizeof(tmpdir), 2); TEST_ASSERT_NOT_NULL(p); /* Write enough entries and checkpoint once */ for (int i = 0; i < 5; i++) persist_wal_async(p, WAL_JOB_UPDATE, "trim", 4); TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p)); /* Write a few more entries then checkpoint again */ for (int i = 0; i < 3; i++) persist_wal_async(p, WAL_JOB_UPDATE, "trim2", 5); TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p)); /* After two checkpoints the wal directory must still be functional * (at least one segment exists — the currently-active one). */ char wal_path[512]; snprintf(wal_path, sizeof(wal_path), "%s/e2e-ckpt/data/wal", tmpdir); int n = count_files_suffix(wal_path, ".wal"); TEST_ASSERT_TRUE(n >= 1); teardown_ckpt_e2e(p); printf("test_checkpoint_wal_trim_after_checkpoint: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 3: snapshot rotation keeps at most snap_keep_n files */ /* ------------------------------------------------------------------ */ static void test_snapshot_rotation_keeps_snap_keep_n(void) { char tmpdir[256]; persist_t* p = make_ckpt_e2e(tmpdir, sizeof(tmpdir), 2); TEST_ASSERT_NOT_NULL(p); /* Run 4 checkpoints with snap_keep_n = 2 */ for (int i = 0; i < 4; i++) { persist_wal_async(p, WAL_JOB_UPDATE, "r", 1); TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p)); } char snap_path[512]; snprintf(snap_path, sizeof(snap_path), "%s/e2e-ckpt/data/snap", tmpdir); int n = count_files_suffix(snap_path, ".capnp"); TEST_ASSERT_TRUE(n <= (int)p->config.snap_keep_n); teardown_ckpt_e2e(p); printf("test_snapshot_rotation_keeps_snap_keep_n: PASS\n"); } /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ void test_e2e_checkpoint(void) { printf("\n>> P0-15b: Checkpoint E2E — round trip\n\n"); RUN_TEST(test_checkpoint_recovery_lsn_continuity); RUN_TEST(test_checkpoint_wal_trim_after_checkpoint); RUN_TEST(test_snapshot_rotation_keeps_snap_keep_n); }