feat: P2-4 wire repl_broadcast into persist_wal_async/sync_append; last_replicated_crc

- 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
This commit is contained in:
Rongsong Shen 2026-05-07 17:54:48 +00:00
parent d326ee1b2e
commit 3563d7387b
7 changed files with 228 additions and 41 deletions

View file

@ -25,6 +25,7 @@ extern void test_ha_redirect(void);
extern void test_p1_integration(void);
extern void test_repl_unit(void);
extern void test_repl_apply(void);
extern void test_persist_repl(void);
void setUp(void) { return; }
@ -119,6 +120,7 @@ int main(int argc, char* argv[])
TEST(test_p1_integration);
TEST(test_repl_unit);
TEST(test_repl_apply);
TEST(test_persist_repl);
return UNITY_END();
}

View file

@ -0,0 +1,145 @@
/*
* 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);
}