myxce/src/controller/testsuite/test_repl_apply.c
Rongsong Shen 6df3bd5906 feat: implement job state, queue state, and worker recovery in WAL replay
WAL payloads:
- Job state transitions (START/FINISH/FAIL/STOP/RESUME/UPDATE etc):
  wal_jobstate_t {uint64_t jobid, uint32_t state} — restores exact state flags
- Queue state (QUEUE_CTRL): wal_queuehdr_t {uint32_t status} + name string —
  re-applies admin open/close/active/inactive after config queues are built
- Worker CTRL: existing [uint32_t detail][capnp xce_worker_t] unchanged

New recovery functions:
- restore_job_state(c, jobid, state): looks up job, overwrites state flags
- restore_queue_state(c, name, status): looks up queue, overwrites status

apply_wal_entry: dispatches all WAL types including job state and queue
test_repl_apply.c: weak stubs for restore_job_state/restore_queue_state
  so unit test binary links without job.c/queue.c
2026-05-08 08:19:20 +00:00

211 lines
6.4 KiB
C

/*
* test_repl_apply.c — P2-2: Tests for apply_wal_entry() standby WAL replay.
*
* TDD RED phase: written BEFORE the implementation.
* These tests verify:
* - LSN advances past replayed entry
* - WAL_CHECKPOINT entries are skipped (rc=0, lsn unchanged)
* - Oversized payloads are rejected (rc != 0)
* - Unknown types (> WAL_ENTRY_TYPE_MAX) are skipped with rc=0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "config.h"
#include "persist.h"
/* ------------------------------------------------------------------ */
/* Weak stubs for P0 entity-restore functions (job.c / worker.c). */
/* The unit test binary does not link those translation units — they */
/* pull in the full cluster.h / netio.h chain. apply_wal_entry() only */
/* calls these when p->cluster != NULL; tests use NULL cluster, so the */
/* weak stubs are never reached. The linker needs the symbols to link. */
/* ------------------------------------------------------------------ */
typedef struct cluster_s cluster_t;
__attribute__((weak))
int restore_job(cluster_t* c, uint64_t jobid,
const void* capnp_payload, size_t len)
{
(void)c; (void)jobid; (void)capnp_payload; (void)len;
return 0;
}
__attribute__((weak))
int worker_from_capnp(cluster_t* c, const void* capnp_payload, size_t len)
{
(void)c; (void)capnp_payload; (void)len;
return 0;
}
__attribute__((weak))
int restore_job_state(cluster_t* c, uint64_t jobid, uint32_t state)
{
(void)c; (void)jobid; (void)state;
return 0;
}
__attribute__((weak))
int restore_queue_state(cluster_t* c, const char* name, uint32_t status)
{
(void)c; (void)name; (void)status;
return 0;
}
/* ------------------------------------------------------------------ */
/* Test helpers */
/* ------------------------------------------------------------------ */
static persist_t* make_test_persist_for_apply(void)
{
persist_t* p = calloc(1, sizeof(persist_t));
if (!p) {
fprintf(stderr, "calloc failed\n");
exit(1);
}
persist_config_t cfg = persist_config_defaults();
cfg.wal_sync_ms = 5000; /* slow fsync — we don't need it in unit tests */
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/xce_apply_XXXXXX");
if (!mkdtemp(tmpdir)) {
fprintf(stderr, "mkdtemp failed\n");
free(p);
exit(1);
}
persist_set_workdir(p, tmpdir, "ctrl-apply");
persist_init(p, &cfg, NULL); /* NULL cluster — entity stubs gate on p->cluster */
persist_start(p);
return p;
}
static void destroy_test_persist(persist_t* p)
{
persist_stop(p);
persist_fini(p);
free(p);
}
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
static void test_apply_wal_entry_advances_lsn(void)
{
persist_t* p = make_test_persist_for_apply();
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.type = WAL_JOB_UPDATE;
hdr.lsn = 42;
hdr.term = 1;
hdr.payload_len = 4;
hdr.crc = 0xDEADBEEF;
hdr.prev_crc = WAL_CRC_NONE;
uint8_t payload[4] = {1, 2, 3, 4};
int rc = apply_wal_entry(p, &hdr, payload);
TEST_ASSERT_EQUAL_INT(0, rc);
/* current_lsn must have advanced to at least lsn+1 */
TEST_ASSERT_TRUE(p->current_lsn > 42);
/* last_crc must reflect the entry's crc */
TEST_ASSERT_EQUAL_UINT32(0xDEADBEEF, p->last_crc);
destroy_test_persist(p);
printf("test_apply_wal_entry_advances_lsn: PASS\n");
}
static void test_apply_wal_entry_skips_checkpoint(void)
{
persist_t* p = make_test_persist_for_apply();
uint64_t lsn_before = p->current_lsn;
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.type = WAL_CHECKPOINT;
hdr.lsn = 100;
hdr.term = 1;
hdr.payload_len = 0;
int rc = apply_wal_entry(p, &hdr, NULL);
TEST_ASSERT_EQUAL_INT(0, rc); /* skip = success, not error */
/* Checkpoint entries are local-only markers — standbys never receive them.
* current_lsn must NOT advance: skip means no-op. */
TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn);
destroy_test_persist(p);
printf("test_apply_wal_entry_skips_checkpoint: PASS\n");
}
static void test_apply_wal_entry_rejects_oversized(void)
{
persist_t* p = make_test_persist_for_apply();
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.type = WAL_JOB_SUBMIT;
hdr.lsn = 1;
hdr.term = 1;
hdr.payload_len = MAX_WAL_PAYLOAD_BYTES + 1; /* too large */
int rc = apply_wal_entry(p, &hdr, NULL);
TEST_ASSERT_NOT_EQUAL(0, rc); /* must be rejected */
destroy_test_persist(p);
printf("test_apply_wal_entry_rejects_oversized: PASS\n");
}
static void test_apply_wal_entry_skips_unknown_type(void)
{
persist_t* p = make_test_persist_for_apply();
uint64_t lsn_before = p->current_lsn;
/*
* Type 200 is above WAL_ENTRY_TYPE_MAX (100).
* Spec: log WARN and skip with rc=0 (forward compatibility).
*/
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.type = 200;
hdr.lsn = 5;
hdr.term = 1;
hdr.payload_len = 0;
int rc = apply_wal_entry(p, &hdr, NULL);
TEST_ASSERT_EQUAL_INT(0, rc); /* unknown type: skip not error */
/* LSN must NOT advance for a skipped-unknown type */
TEST_ASSERT_EQUAL_UINT64(lsn_before, p->current_lsn);
destroy_test_persist(p);
printf("test_apply_wal_entry_skips_unknown_type: PASS\n");
}
static void test_apply_wal_entry_null_guard(void)
{
/* Both NULL inputs must return non-zero (EINVAL) */
int rc = apply_wal_entry(NULL, NULL, NULL);
TEST_ASSERT_NOT_EQUAL(0, rc);
printf("test_apply_wal_entry_null_guard: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point (registered in controller_test.c) */
/* ------------------------------------------------------------------ */
void test_repl_apply(void)
{
RUN_TEST(test_apply_wal_entry_advances_lsn);
RUN_TEST(test_apply_wal_entry_skips_checkpoint);
RUN_TEST(test_apply_wal_entry_rejects_oversized);
RUN_TEST(test_apply_wal_entry_skips_unknown_type);
RUN_TEST(test_apply_wal_entry_null_guard);
}