/* P0-15c: Hook E2E — ordering and WAL write * * Exercises cross-phase interactions: * - Hook functions (P0-D) write WAL entries (P0-B) * - LSN advances monotonically for each hook call * - WAL segment contains entries with correct type field */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "persist.h" #include "xce/cluster_def.h" /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static char g_e2e_hooks_tmpdir[256]; static persist_t* make_e2e_hook_persist(void) { strcpy(g_e2e_hooks_tmpdir, "/tmp/xce_e2e_hook_XXXXXX"); if (!mkdtemp(g_e2e_hooks_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 = 1; /* flush quickly */ persist_set_workdir(p, g_e2e_hooks_tmpdir, "e2e-hook"); 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_e2e_hook(persist_t* p) { persist_stop(p); persist_fini(p); free(p); } /* Find the first .wal segment in a WAL directory. */ static int find_first_seg(const char* wal_dir, char* out, size_t outsz) { DIR* d = opendir(wal_dir); if (!d) return -1; struct dirent* e; while ((e = readdir(d)) != NULL) { size_t len = strlen(e->d_name); if (len > 4 && strcmp(e->d_name + len - 4, ".wal") == 0) { snprintf(out, outsz, "%s/%s", wal_dir, e->d_name); closedir(d); return 0; } } closedir(d); return -1; } /* Read the Nth (0-indexed) wal_entry_hdr_t from a segment file (sequential * layout: hdr + payload). Returns 0 on success. */ static int read_nth_hdr(const char* seg_path, int n, wal_entry_hdr_t* out) { int fd = open(seg_path, O_RDONLY); if (fd < 0) return -1; off_t offset = 0; for (int i = 0; i <= n; i++) { wal_entry_hdr_t hdr; ssize_t rd = pread(fd, &hdr, sizeof(hdr), offset); if (rd != (ssize_t)sizeof(hdr)) { close(fd); return -1; } if (i == n) { *out = hdr; close(fd); return 0; } offset += (off_t)(sizeof(hdr) + hdr.payload_len); } close(fd); return -1; } /* ------------------------------------------------------------------ */ /* Test 1: job hook writes WAL before LSN advances */ /* ------------------------------------------------------------------ */ static void test_job_hook_writes_wal_before_lsn_advances(void) { persist_t* p = make_e2e_hook_persist(); TEST_ASSERT_NOT_NULL(p); uint64_t lsn_before = p->current_lsn; int rc = persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_NEW, p, NULL); TEST_ASSERT_EQUAL_INT(0, rc); /* LSN must have advanced by exactly 1 */ TEST_ASSERT_EQUAL_UINT64(lsn_before + 1, p->current_lsn); /* Flush and verify the entry type in the segment file */ TEST_ASSERT_EQUAL_INT(0, persist_flush(p)); char seg_path[512]; TEST_ASSERT_EQUAL_INT(0, find_first_seg(p->wal_path, seg_path, sizeof(seg_path))); wal_entry_hdr_t hdr; TEST_ASSERT_EQUAL_INT(0, read_nth_hdr(seg_path, 0, &hdr)); TEST_ASSERT_EQUAL_UINT32(WAL_JOB_SUBMIT, hdr.type); teardown_e2e_hook(p); printf("test_job_hook_writes_wal_before_lsn_advances: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 2: worker hook writes WAL_WORKER_CTRL async */ /* ------------------------------------------------------------------ */ static void test_worker_hook_writes_wal_async(void) { persist_t* p = make_e2e_hook_persist(); TEST_ASSERT_NOT_NULL(p); uint64_t lsn_before = p->current_lsn; int rc = persist_worker_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, HOOK_DETAIL_WORKER_JOIN, p, NULL); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_UINT64(lsn_before + 1, p->current_lsn); TEST_ASSERT_EQUAL_INT(0, persist_flush(p)); char seg_path[512]; TEST_ASSERT_EQUAL_INT(0, find_first_seg(p->wal_path, seg_path, sizeof(seg_path))); wal_entry_hdr_t hdr; TEST_ASSERT_EQUAL_INT(0, read_nth_hdr(seg_path, 0, &hdr)); TEST_ASSERT_EQUAL_UINT32(WAL_WORKER_CTRL, hdr.type); teardown_e2e_hook(p); printf("test_worker_hook_writes_wal_async: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 3: hook sequence preserves monotone LSN order */ /* ------------------------------------------------------------------ */ static void test_hook_sequence_preserves_lsn_order(void) { persist_t* p = make_e2e_hook_persist(); TEST_ASSERT_NOT_NULL(p); uint64_t lsn0 = p->current_lsn; persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_NEW, p, NULL); uint64_t lsn1 = p->current_lsn; persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_START, p, NULL); uint64_t lsn2 = p->current_lsn; persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_EXIT, p, NULL); uint64_t lsn3 = p->current_lsn; /* LSNs must be strictly increasing */ TEST_ASSERT_TRUE(lsn1 > lsn0); TEST_ASSERT_TRUE(lsn2 > lsn1); TEST_ASSERT_TRUE(lsn3 > lsn2); TEST_ASSERT_EQUAL_UINT64(lsn0 + 3, lsn3); /* Flush and verify three entries on disk in order */ TEST_ASSERT_EQUAL_INT(0, persist_flush(p)); char seg_path[512]; TEST_ASSERT_EQUAL_INT(0, find_first_seg(p->wal_path, seg_path, sizeof(seg_path))); wal_entry_hdr_t h0, h1, h2; TEST_ASSERT_EQUAL_INT(0, read_nth_hdr(seg_path, 0, &h0)); TEST_ASSERT_EQUAL_INT(0, read_nth_hdr(seg_path, 1, &h1)); TEST_ASSERT_EQUAL_INT(0, read_nth_hdr(seg_path, 2, &h2)); /* LSNs in the headers must also be strictly increasing */ TEST_ASSERT_TRUE(h1.lsn > h0.lsn); TEST_ASSERT_TRUE(h2.lsn > h1.lsn); teardown_e2e_hook(p); printf("test_hook_sequence_preserves_lsn_order: PASS\n"); } /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ void test_e2e_hooks(void) { printf("\n>> P0-15c: Hook E2E — ordering and WAL write\n\n"); RUN_TEST(test_job_hook_writes_wal_before_lsn_advances); RUN_TEST(test_worker_hook_writes_wal_async); RUN_TEST(test_hook_sequence_preserves_lsn_order); }