- Move repl.h from controller/ to include/ (shared header) to break persist.h ↔ repl.h circular include - Extract wal_entry_hdr_t + wal_entry_type into include/wal_types.h so repl.h can include only what it needs without pulling in all of persist.h - Add replicator_t repl field to persist_t (embedded by value, P2-4) - Wire repl_init/repl_fini into persist_init/persist_fini lifecycle - Wire repl_broadcast into persist_wal_async after each WAL write; last_replicated_crc updated here (not in persist_write_checkpoint_marker) - Wire repl_wait_ack into persist_wal_sync_append; returns 0 on local WAL success; sync_rc_out carries -ENODEV/-ETIMEDOUT for degraded mode - Add 4 TDD tests in test_persist_repl.c: structural repl field check, async no-standbys succeeds, sync degraded mode rc, checkpoint CRC invariant
145 lines
4.8 KiB
C
145 lines
4.8 KiB
C
/*
|
|
* test_persist_repl.c — P2-4: Tests for repl_broadcast wired into WAL write paths.
|
|
*
|
|
* TDD RED phase: these tests must FAIL before implementation.
|
|
* After implementation they must all PASS.
|
|
*
|
|
* Tests verify:
|
|
* 1. persist_t has repl field (structural/compile-time check)
|
|
* 2. persist_wal_async succeeds with zero standbys
|
|
* 3. persist_wal_sync_append returns 0 (WAL success) but sets sync_rc < 0 (degraded)
|
|
* 4. last_replicated_crc NOT updated by WAL_CHECKPOINT (checkpoint marker)
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
#include "persist.h"
|
|
#include "repl.h"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static persist_t* make_repl_persist(const char* suffix)
|
|
{
|
|
persist_t* p = calloc(1, sizeof(persist_t));
|
|
persist_config_t cfg = persist_config_defaults();
|
|
cfg.wal_sync_ms = 5000;
|
|
cfg.sync_timeout_ms = 50; /* short timeout so sync tests don't hang */
|
|
|
|
char tmpdir[256];
|
|
snprintf(tmpdir, sizeof(tmpdir), "/tmp/xce_repl_%s_XXXXXX", suffix);
|
|
if (!mkdtemp(tmpdir)) {
|
|
free(p);
|
|
return NULL;
|
|
}
|
|
|
|
persist_set_workdir(p, tmpdir, "ctrl");
|
|
persist_init(p, &cfg, NULL);
|
|
persist_start(p);
|
|
return p;
|
|
}
|
|
|
|
static void teardown_repl_persist(persist_t* p)
|
|
{
|
|
if (!p) return;
|
|
persist_stop(p);
|
|
persist_fini(p);
|
|
free(p);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/*
|
|
* Structural: persist_t must contain a repl field of type replicator_t.
|
|
* This is a compile-time check — accessing p.repl.nstandbys must compile.
|
|
*/
|
|
static void test_persist_has_repl_field(void)
|
|
{
|
|
persist_t p;
|
|
memset(&p, 0, sizeof(p));
|
|
/* Accessing repl.nstandbys is the compile-time guard */
|
|
TEST_ASSERT_EQUAL_INT(0, p.repl.nstandbys);
|
|
printf("test_persist_has_repl_field: PASS\n");
|
|
}
|
|
|
|
/*
|
|
* WAL async must succeed when there are zero standbys.
|
|
* repl_broadcast is a stub; no replication occurs, but write must not fail.
|
|
*/
|
|
static void test_wal_async_no_standbys_succeeds(void)
|
|
{
|
|
persist_t* p = make_repl_persist("async");
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
int rc = persist_wal_async(p, WAL_JOB_UPDATE, "payload", 7);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
teardown_repl_persist(p);
|
|
printf("test_wal_async_no_standbys_succeeds: PASS\n");
|
|
}
|
|
|
|
/*
|
|
* persist_wal_sync_append must return 0 (WAL write succeeded locally) even
|
|
* when there are no standbys (degraded mode). The sync_rc_out parameter
|
|
* must be set to a negative value to signal degraded mode
|
|
* (-ENODEV = no standbys, or -ETIMEDOUT = timeout).
|
|
*/
|
|
static void test_wal_sync_no_standbys_returns_enodev_in_sync_rc(void)
|
|
{
|
|
persist_t* p = make_repl_persist("sync");
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
int sync_rc = 0;
|
|
int rc = persist_wal_sync_append(p, WAL_JOB_SUBMIT, "job_payload", 11,
|
|
&sync_rc);
|
|
/* WAL write itself must succeed */
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
/* sync_rc: -ENODEV (no standbys) or -ETIMEDOUT — either indicates degraded */
|
|
TEST_ASSERT_TRUE(sync_rc < 0);
|
|
|
|
teardown_repl_persist(p);
|
|
printf("test_wal_sync_no_standbys_returns_enodev_in_sync_rc: PASS\n");
|
|
}
|
|
|
|
/*
|
|
* WAL_CHECKPOINT (via persist_checkpoint) must NOT update last_replicated_crc.
|
|
* Checkpoint markers are local-only; the replication CRC chain must remain
|
|
* unbroken for standbys (design doc Part 2.3).
|
|
*/
|
|
static void test_last_replicated_crc_not_updated_for_checkpoint(void)
|
|
{
|
|
persist_t* p = make_repl_persist("crc");
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
|
|
uint32_t crc_before = p->last_replicated_crc;
|
|
|
|
/* persist_checkpoint writes a WAL_CHECKPOINT marker then triggers a snap.
|
|
* Neither the marker write nor snap must update last_replicated_crc. */
|
|
persist_checkpoint(p);
|
|
|
|
/* last_replicated_crc must be unchanged from before the checkpoint */
|
|
TEST_ASSERT_EQUAL_UINT32(crc_before, p->last_replicated_crc);
|
|
|
|
teardown_repl_persist(p);
|
|
printf("test_last_replicated_crc_not_updated_for_checkpoint: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Entry point */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void test_persist_repl(void)
|
|
{
|
|
RUN_TEST(test_persist_has_repl_field);
|
|
RUN_TEST(test_wal_async_no_standbys_succeeds);
|
|
RUN_TEST(test_wal_sync_no_standbys_returns_enodev_in_sync_rc);
|
|
RUN_TEST(test_last_replicated_crc_not_updated_for_checkpoint);
|
|
}
|