feat: persistence hook functions wired to WAL write on AFTER_EVENT (P0-6)

Implements persist_job_hook_fun, persist_queue_hook_fun, and
persist_worker_hook_fun — AFTER_EVENT hooks that write WAL entries
whenever state mutations occur in the controller.

- detail_to_wal_type() maps all 15 HOOK_DETAIL_* values to WAL types;
  unknown details return 0 and are silently skipped
- HOOK_DETAIL_JOB_NEW uses the sync replication path (WAL_JOB_SUBMIT);
  all others use persist_wal_async
- P0 payload: 4-byte detail value; full Cap'n Proto serialization in Task 15
- Hook signature matches hook_fn_t exactly:
  int fn(int type, int situation, int detail, void* cls, va_list ap)
- Added <stdarg.h> to persist.h for va_list; added hook.h and
  xce/cluster_def.h includes to persist.c
- 18 new unit tests in test_persist_hooks.c covering all 15 detail→WAL
  mappings, skip-on-unknown, and sequential LSN advancement
This commit is contained in:
Rongsong Shen 2026-05-07 12:28:20 +00:00
parent 045d9e3287
commit 6f3b98c6b5
5 changed files with 500 additions and 0 deletions

View file

@ -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();
}

View file

@ -0,0 +1,391 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
#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);
}