diff --git a/src/controller/meson.build b/src/controller/meson.build index d09abc4..4612d5a 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -74,6 +74,7 @@ controller_unit_test_src = [ 'testsuite/test_persist_checkpoint.c', 'testsuite/test_persist_recovery.c', 'testsuite/test_hooks.c', + 'testsuite/test_persist_hooks.c', 'persist.c', ] diff --git a/src/controller/persist.c b/src/controller/persist.c index 1c3e138..cbc6456 100644 --- a/src/controller/persist.c +++ b/src/controller/persist.c @@ -1,6 +1,8 @@ #define _GNU_SOURCE #include "persist.h" #include "config.h" +#include "hook.h" +#include "xce/cluster_def.h" #include #include @@ -785,3 +787,89 @@ void persist_log_entry_metadata(const wal_entry_hdr_t* hdr) hdr->payload_len, hdr->crc); } + +/* ------------------------------------------------------------------ */ +/* Hook functions — AFTER_EVENT handlers that write WAL entries */ +/* ------------------------------------------------------------------ */ + +/* Map HOOK_DETAIL_* values to WAL entry types. + * Returns 0 for details that have no WAL representation (skip). */ +static uint32_t detail_to_wal_type(int detail) +{ + switch (detail) { + case HOOK_DETAIL_JOB_NEW: return WAL_JOB_SUBMIT; + case HOOK_DETAIL_JOB_START: return WAL_JOB_START; + case HOOK_DETAIL_JOB_EXIT: return WAL_JOB_FINISH; + case HOOK_DETAIL_JOB_FAIL: return WAL_JOB_FAIL; + case HOOK_DETAIL_JOB_STOP: return WAL_JOB_STOP; + case HOOK_DETAIL_JOB_RESUME: return WAL_JOB_RESUME; + case HOOK_DETAIL_JOB_UPDATE: return WAL_JOB_UPDATE; + case HOOK_DETAIL_JOB_CKPT: return WAL_JOB_CKPT; + case HOOK_DETAIL_JOB_RUSAGE_UPDATE: return WAL_JOB_RUSAGE; + case HOOK_DETAIL_SETPREASON: return WAL_JOB_PREASON; + case HOOK_DETAIL_CLRPREASON: return WAL_JOB_PREASON; + case HOOK_DETAIL_QUEUE_UPDATE: return WAL_QUEUE_CTRL; + case HOOK_DETAIL_WORKER_JOIN: return WAL_WORKER_CTRL; + case HOOK_DETAIL_WORKER_UPDATE: return WAL_WORKER_CTRL; + case HOOK_DETAIL_WORKER_DEAD: return WAL_WORKER_CTRL; + default: return 0; /* unknown — skip */ + } +} + +/* P0 payload: just the detail value (4 bytes). + * Full Cap'n Proto serialization is wired in Task 15. */ + +int persist_job_hook_fun(int type, int situation, int detail, + void* cls, va_list ap) +{ + (void)type; + (void)situation; + (void)ap; + + persist_t* p = cls; + uint32_t wal_type = detail_to_wal_type(detail); + if (wal_type == 0) + return 0; /* unknown detail — skip */ + + uint32_t payload = (uint32_t)detail; + + if (wal_type == WAL_JOB_SUBMIT) { + /* Job submission uses sync replication path */ + int sync_rc = 0; + persist_wal_sync_append(p, wal_type, &payload, sizeof(payload), &sync_rc); + return sync_rc; + } + return persist_wal_async(p, wal_type, &payload, sizeof(payload)); +} + +int persist_queue_hook_fun(int type, int situation, int detail, + void* cls, va_list ap) +{ + (void)type; + (void)situation; + (void)ap; + + persist_t* p = cls; + uint32_t wal_type = detail_to_wal_type(detail); + if (wal_type == 0) + return 0; + + uint32_t payload = (uint32_t)detail; + return persist_wal_async(p, wal_type, &payload, sizeof(payload)); +} + +int persist_worker_hook_fun(int type, int situation, int detail, + void* cls, va_list ap) +{ + (void)type; + (void)situation; + (void)ap; + + persist_t* p = cls; + uint32_t wal_type = detail_to_wal_type(detail); + if (wal_type == 0) + return 0; + + uint32_t payload = (uint32_t)detail; + return persist_wal_async(p, wal_type, &payload, sizeof(payload)); +} diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index 4334905..4d95725 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -11,6 +11,7 @@ extern void test_persist_schema(void); extern void test_persist_checkpoint(void); extern void test_persist_recovery(void); extern void test_hooks(void); +extern void test_persist_hooks(void); void setUp(void) { return; } @@ -91,6 +92,7 @@ int main(int argc, char* argv[]) TEST(test_persist_checkpoint); TEST(test_persist_recovery); TEST(test_hooks); + TEST(test_persist_hooks); return UNITY_END(); } diff --git a/src/controller/testsuite/test_persist_hooks.c b/src/controller/testsuite/test_persist_hooks.c new file mode 100644 index 0000000..981d019 --- /dev/null +++ b/src/controller/testsuite/test_persist_hooks.c @@ -0,0 +1,391 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "config.h" +#include "persist.h" +#include "xce/cluster_def.h" + +/* ------------------------------------------------------------------ */ +/* Test helpers */ +/* ------------------------------------------------------------------ */ + +static char g_tmpdir[256]; + +static persist_t* make_hook_test_persist(void) +{ + strcpy(g_tmpdir, "/tmp/xce_hook_test_XXXXXX"); + if (!mkdtemp(g_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; /* 1 ms — fsync thread wakes up immediately on stop */ + + persist_set_workdir(p, g_tmpdir, "hook-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 cleanup_hook_test_persist(persist_t* p) +{ + persist_stop(p); + persist_fini(p); + free(p); +} + +/* ------------------------------------------------------------------ */ +/* Tests — RED phase: functions not yet implemented, must fail to link */ +/* ------------------------------------------------------------------ */ + +static void test_job_hook_new_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_persist(); + TEST_ASSERT_NOT_NULL(p); + + uint64_t lsn_before = p->current_lsn; + + /* Call hook directly — bypasses hook registration system for unit test. + * Signature: int hook_fn(int type, int situation, int detail, + * void* data, va_list ap) + * We pass NULL for ap (no variadic args used in P0 implementation). */ + 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 — WAL entry was written */ + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_new_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_start_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_START, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_start_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_exit_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_EXIT, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_exit_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_fail_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_FAIL, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_fail_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_stop_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_STOP, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_stop_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_resume_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_RESUME, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_resume_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_update_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_UPDATE, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_update_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_ckpt_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_CKPT, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_ckpt_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_rusage_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_RUSAGE_UPDATE, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_rusage_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_setpreason_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_SETPREASON, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_setpreason_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_clrpreason_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_CLRPREASON, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_clrpreason_writes_wal_entry: PASS\n"); +} + +static void test_queue_hook_update_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_persist(); + TEST_ASSERT_NOT_NULL(p); + + uint64_t lsn_before = p->current_lsn; + + int rc = persist_queue_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, + HOOK_DETAIL_QUEUE_UPDATE, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_queue_hook_update_writes_wal_entry: PASS\n"); +} + +static void test_worker_hook_join_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_worker_hook_join_writes_wal_entry: PASS\n"); +} + +static void test_worker_hook_update_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_UPDATE, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_worker_hook_update_writes_wal_entry: PASS\n"); +} + +static void test_worker_hook_dead_writes_wal_entry(void) +{ + persist_t* p = make_hook_test_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_DEAD, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_GREATER_THAN(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_worker_hook_dead_writes_wal_entry: PASS\n"); +} + +static void test_job_hook_skips_unknown_detail(void) +{ + persist_t* p = make_hook_test_persist(); + TEST_ASSERT_NOT_NULL(p); + + uint64_t lsn_before = p->current_lsn; + + /* HOOK_DETAIL_SCHEDULER_JOIN has no WAL mapping — must be silently skipped */ + int rc = persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, + HOOK_DETAIL_SCHEDULER_JOIN, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* LSN must NOT have advanced — no WAL entry written */ + TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_skips_unknown_detail: PASS\n"); +} + +static void test_worker_hook_skips_unknown_detail(void) +{ + persist_t* p = make_hook_test_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_SCHEDULER_DEAD, p, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_worker_hook_skips_unknown_detail: PASS\n"); +} + +static void test_job_hook_new_writes_multiple_entries_sequentially(void) +{ + persist_t* p = make_hook_test_persist(); + TEST_ASSERT_NOT_NULL(p); + + uint64_t lsn_before = p->current_lsn; + + /* Fire three hook calls — each must advance LSN */ + persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, + HOOK_DETAIL_JOB_NEW, p, NULL); + persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, + HOOK_DETAIL_JOB_START, p, NULL); + persist_job_hook_fun(HOOK_AFTER_EVENT, HOOK_AFTER_EVENT, + HOOK_DETAIL_JOB_EXIT, p, NULL); + + TEST_ASSERT_EQUAL_UINT64(lsn_before + 3, p->current_lsn); + + cleanup_hook_test_persist(p); + printf("test_job_hook_new_writes_multiple_entries_sequentially: PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Suite entry point */ +/* ------------------------------------------------------------------ */ + +void test_persist_hooks(void) +{ + printf("\n>> persist hook function tests\n\n"); + + RUN_TEST(test_job_hook_new_writes_wal_entry); + RUN_TEST(test_job_hook_start_writes_wal_entry); + RUN_TEST(test_job_hook_exit_writes_wal_entry); + RUN_TEST(test_job_hook_fail_writes_wal_entry); + RUN_TEST(test_job_hook_stop_writes_wal_entry); + RUN_TEST(test_job_hook_resume_writes_wal_entry); + RUN_TEST(test_job_hook_update_writes_wal_entry); + RUN_TEST(test_job_hook_ckpt_writes_wal_entry); + RUN_TEST(test_job_hook_rusage_writes_wal_entry); + RUN_TEST(test_job_hook_setpreason_writes_wal_entry); + RUN_TEST(test_job_hook_clrpreason_writes_wal_entry); + RUN_TEST(test_queue_hook_update_writes_wal_entry); + RUN_TEST(test_worker_hook_join_writes_wal_entry); + RUN_TEST(test_worker_hook_update_writes_wal_entry); + RUN_TEST(test_worker_hook_dead_writes_wal_entry); + RUN_TEST(test_job_hook_skips_unknown_detail); + RUN_TEST(test_worker_hook_skips_unknown_detail); + RUN_TEST(test_job_hook_new_writes_multiple_entries_sequentially); +} diff --git a/src/include/persist.h b/src/include/persist.h index 4d8af2b..01afb7c 100644 --- a/src/include/persist.h +++ b/src/include/persist.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -145,3 +146,20 @@ int persist_checkpoint(persist_t* p); /* Logging helper — never logs payload content (security invariant) */ void persist_log_entry_metadata(const wal_entry_hdr_t* hdr); + +/* ============================================================ + * AFTER_EVENT hook functions — registered by persist_start() + * + * Signature matches hook_fn_t: int fn(int type, int situation, + * int detail, void* data, va_list ap) + * cls/data points to the persist_t instance. + * + * P0: writes a minimal 4-byte payload (detail value) to the WAL. + * Full Cap'n Proto serialization is wired in Task 15. + * ============================================================ */ +int persist_job_hook_fun(int type, int situation, int detail, + void* cls, va_list ap); +int persist_queue_hook_fun(int type, int situation, int detail, + void* cls, va_list ap); +int persist_worker_hook_fun(int type, int situation, int detail, + void* cls, va_list ap);