- persist_write_checkpoint_marker(): writes WAL_CHECKPOINT header entry
with CRC, increments current_lsn; never updates last_replicated_crc
(checkpoint markers are local-only, never replicated)
- write_snapshot(): atomic tmp→rename write of P0 sentinel snapshot
("XCESNAP\0" + checkpoint_lsn); full Cap'n Proto body in Task 11
- trim_wal_segments(): removes seg-*.wal files whose start LSN is
strictly less than checkpoint_lsn
- rotate_snapshots(): collects .capnp files, sorts by name (LSN order),
deletes oldest beyond snap_keep_n
- persist_checkpoint(): EBUSY guard via atomic_exchange, sequences
marker→lsn record→snapshot→trim→rotate, clears flag on exit
- 5 TDD tests in test_persist_checkpoint.c covering: snapshot file
creation, checkpoint_lsn advance, WAL lsn advance, EBUSY guard,
and rotation keeping at most snap_keep_n files
- All 18 controller unit tests pass (0 failures)
198 lines
6.2 KiB
C
198 lines
6.2 KiB
C
#define _GNU_SOURCE
|
|
#include <assert.h>
|
|
#include <dirent.h>
|
|
#include <errno.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"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test-local helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Count files with a given suffix in a directory */
|
|
static int count_files_with_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;
|
|
}
|
|
|
|
static persist_t* make_ckpt_persist(char* tmpdir_buf, size_t buf_sz)
|
|
{
|
|
snprintf(tmpdir_buf, buf_sz, "/tmp/xce_ckpt_XXXXXX");
|
|
if (!mkdtemp(tmpdir_buf)) 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; /* slow background fsync — prevent races */
|
|
cfg.snap_keep_n = 2;
|
|
|
|
persist_set_workdir(p, tmpdir_buf, "ctrl-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_ckpt_persist(persist_t* p)
|
|
{
|
|
persist_stop(p);
|
|
persist_fini(p);
|
|
free(p);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 1: checkpoint creates at least one snapshot file */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_checkpoint_creates_snapshot_file(void)
|
|
{
|
|
char tmpdir[256];
|
|
persist_t* p = make_ckpt_persist(tmpdir, sizeof(tmpdir));
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
/* Write a WAL entry so there is something to checkpoint */
|
|
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "data", 4));
|
|
|
|
/* Trigger checkpoint */
|
|
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p));
|
|
|
|
/* Verify at least one .capnp snapshot file was created */
|
|
char snap_path[512];
|
|
snprintf(snap_path, sizeof(snap_path), "%s/ctrl-test/data/snap", tmpdir);
|
|
int n = count_files_with_suffix(snap_path, ".capnp");
|
|
TEST_ASSERT_TRUE(n >= 1);
|
|
|
|
cleanup_ckpt_persist(p);
|
|
printf("test_checkpoint_creates_snapshot_file: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 2: checkpoint advances checkpoint_lsn */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_checkpoint_sets_checkpoint_lsn(void)
|
|
{
|
|
char tmpdir[256];
|
|
persist_t* p = make_ckpt_persist(tmpdir, sizeof(tmpdir));
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
/* Write 3 WAL entries */
|
|
for (int i = 0; i < 3; i++)
|
|
persist_wal_async(p, WAL_JOB_UPDATE, "x", 1);
|
|
|
|
uint64_t lsn_before = p->current_lsn;
|
|
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p));
|
|
|
|
/* checkpoint_lsn must have advanced to at least lsn_before */
|
|
TEST_ASSERT_TRUE(p->checkpoint_lsn >= lsn_before);
|
|
|
|
cleanup_ckpt_persist(p);
|
|
printf("test_checkpoint_sets_checkpoint_lsn: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 3: checkpoint writes a WAL_CHECKPOINT marker (advances lsn) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_checkpoint_writes_marker_to_wal(void)
|
|
{
|
|
char tmpdir[256];
|
|
persist_t* p = make_ckpt_persist(tmpdir, sizeof(tmpdir));
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
uint64_t lsn_before = p->current_lsn;
|
|
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p));
|
|
|
|
/* current_lsn must have advanced — WAL_CHECKPOINT marker was written */
|
|
TEST_ASSERT_TRUE(p->current_lsn > lsn_before);
|
|
|
|
cleanup_ckpt_persist(p);
|
|
printf("test_checkpoint_writes_marker_to_wal: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 4: concurrent checkpoint returns -EBUSY */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_checkpoint_busy_guard(void)
|
|
{
|
|
char tmpdir[256];
|
|
persist_t* p = make_ckpt_persist(tmpdir, sizeof(tmpdir));
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
/* Simulate a checkpoint already in-progress */
|
|
atomic_store(&p->checkpoint_in_progress, 1);
|
|
|
|
int rc = persist_checkpoint(p);
|
|
TEST_ASSERT_EQUAL_INT(-EBUSY, rc);
|
|
|
|
/* Clean up: allow stop to proceed */
|
|
atomic_store(&p->checkpoint_in_progress, 0);
|
|
cleanup_ckpt_persist(p);
|
|
printf("test_checkpoint_busy_guard: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 5: snapshot rotation keeps at most snap_keep_n files */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_checkpoint_rotation_keeps_snap_keep_n(void)
|
|
{
|
|
char tmpdir[256];
|
|
persist_t* p = make_ckpt_persist(tmpdir, sizeof(tmpdir));
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
/* snap_keep_n is 2; run 4 checkpoints */
|
|
for (int i = 0; i < 4; i++) {
|
|
persist_wal_async(p, WAL_JOB_UPDATE, "y", 1);
|
|
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p));
|
|
}
|
|
|
|
char snap_path[512];
|
|
snprintf(snap_path, sizeof(snap_path), "%s/ctrl-test/data/snap", tmpdir);
|
|
int n = count_files_with_suffix(snap_path, ".capnp");
|
|
TEST_ASSERT_TRUE(n <= (int)p->config.snap_keep_n);
|
|
|
|
cleanup_ckpt_persist(p);
|
|
printf("test_checkpoint_rotation_keeps_snap_keep_n: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test suite entry point */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void test_persist_checkpoint(void)
|
|
{
|
|
RUN_TEST(test_checkpoint_creates_snapshot_file);
|
|
RUN_TEST(test_checkpoint_sets_checkpoint_lsn);
|
|
RUN_TEST(test_checkpoint_writes_marker_to_wal);
|
|
RUN_TEST(test_checkpoint_busy_guard);
|
|
RUN_TEST(test_checkpoint_rotation_keeps_snap_keep_n);
|
|
}
|