feat(ha-repl-hardening): audit safe commit point and snapshot transfer tests (tasks 6.3, 6.4)

6.3: Fix single-node bug — ha_compute_safe_committed_lsn now returns
self_lsn instead of 0 for n_peers=0 (prevents WAL truncation on
single-node re-election). Add self_lsn/self_tag to ha_elect_ctx_t,
populated unconditionally in ha_start_election_async. All 3 test
scenarios pass under ASAN.

6.4: Add test_snapshot_recv_128kb_two_chunks (128KB snapshot in
2x64KB chunks, verifies size and applied_lsn); 4 other test cases
already existed from tasks 3.5c/3.5d. All ASAN-clean.
This commit is contained in:
Rongsong Shen 2026-06-05 06:21:00 +00:00
parent d41cfc5813
commit 345d51dd61
5 changed files with 160 additions and 23 deletions

View file

@ -1932,6 +1932,113 @@ static void test_snapshot_recv_null_args_fail(void)
printf("test_snapshot_recv_null_args_fail: PASS\n");
}
/* Test 6.4-A: 128KB snapshot in 2 equal chunks (65536 bytes each) — standby
* receives both chunks, verifies total_size matches, snap.part is atomically
* renamed to snap-{lsn}.bin, persist_load_snapshot is called successfully,
* and applied_lsn equals the checkpoint_lsn embedded in the snapshot.
*
* This is the primary task-6.4 coverage test for Req 4.2, 4.3.
* Uses the same 65536-byte chunk size as the sender test
* (test_snapshot_transfer_two_chunks) to exercise the full receive path. */
static void test_snapshot_recv_128kb_two_chunks(void)
{
char tmpdir[] = "/tmp/xcectest_snap128kb_XXXXXX";
TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir));
char snap_dir[512];
snprintf(snap_dir, sizeof(snap_dir), "%s/snap", tmpdir);
TEST_ASSERT_EQUAL_INT(0, mkdir(snap_dir, 0700));
persist_t *p = calloc(1, sizeof(persist_t));
TEST_ASSERT_NOT_NULL(p);
persist_config_t pcfg = persist_config_defaults();
persist_set_workdir(p, tmpdir, "ctrl-snap128kb");
persist_init(p, &pcfg, NULL);
persist_start(p);
ha_state_t ha;
snap_recv_ha_init(&ha);
/* 128KB snapshot split into two 65536-byte chunks */
const uint64_t snap_lsn = 0x64; /* LSN 100 — matches sender test */
const size_t chunk_size = 65536;
const size_t total_size = 131072; /* 128 * 1024 */
/* Allocate and zero-fill chunks */
uint8_t *chunk0 = calloc(1, chunk_size);
uint8_t *chunk1 = calloc(1, chunk_size);
TEST_ASSERT_NOT_NULL(chunk0);
TEST_ASSERT_NOT_NULL(chunk1);
/* Embed valid XCESNAP header in chunk0 so persist_load_snapshot succeeds */
memcpy(chunk0, "XCESNAP\0", 8);
memcpy(chunk0 + 8, &snap_lsn, 8);
/* ── Chunk 0: not last ── */
snapshot_transfer_t c0;
memset(&c0, 0, sizeof(c0));
c0.snapshot_lsn = snap_lsn;
c0.chunk_index = 0;
c0.chunk_offset = 0;
c0.total_size = (uint64_t)total_size;
c0.is_last = 0;
int rc = ha_handle_snapshot_chunk(&ha, p, snap_dir, &c0, chunk0, chunk_size);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "chunk 0 must succeed");
/* snap.part must exist and be exactly chunk_size bytes */
char part_path[600];
snprintf(part_path, sizeof(part_path), "%s/snap.part", snap_dir);
struct stat st;
TEST_ASSERT_EQUAL_INT_MESSAGE(0, stat(part_path, &st),
"snap.part must exist after chunk 0");
TEST_ASSERT_EQUAL_UINT64_MESSAGE((uint64_t)chunk_size, (uint64_t)st.st_size,
"snap.part must be chunk_size bytes after chunk 0");
/* ── Chunk 1: isLast=true ── */
snapshot_transfer_t c1;
memset(&c1, 0, sizeof(c1));
c1.snapshot_lsn = snap_lsn;
c1.chunk_index = 1;
c1.chunk_offset = (uint64_t)chunk_size;
c1.total_size = (uint64_t)total_size;
c1.is_last = 1;
rc = ha_handle_snapshot_chunk(&ha, p, snap_dir, &c1, chunk1, chunk_size);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "chunk 1 (isLast) must succeed");
/* snap.part must be gone (atomically renamed) */
TEST_ASSERT_NOT_EQUAL_MESSAGE(0, stat(part_path, &st),
"snap.part must be gone after isLast rename");
/* Final file snap-{lsn:016x}.bin must exist with total_size bytes */
char final_path[600];
snprintf(final_path, sizeof(final_path),
"%s/snap-%016" PRIx64 ".bin", snap_dir, snap_lsn);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, stat(final_path, &st),
"snap-{lsn}.bin must exist after isLast");
TEST_ASSERT_EQUAL_UINT64_MESSAGE((uint64_t)total_size, (uint64_t)st.st_size,
"snap-{lsn}.bin must be exactly total_size bytes");
/* applied_lsn must equal checkpoint_lsn (snap_lsn) after successful load */
uint64_t got_lsn = (uint64_t)atomic_load_explicit(&ha.applied_lsn,
memory_order_acquire);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(snap_lsn, got_lsn,
"applied_lsn must equal checkpoint_lsn after load");
free(chunk0);
free(chunk1);
ha_fini(&ha);
persist_stop(p);
persist_fini(p);
free(p);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmpdir);
system(cmd);
printf("test_snapshot_recv_128kb_two_chunks: PASS\n");
}
/* Test 3.5d: reconnected standby discards snap.part and restarts from chunk 0.
*
* Design §Snapshot Transfer: "Mid-transfer reconnect always restarts snapshot
@ -2196,6 +2303,8 @@ void test_config_repl(void)
RUN_TEST(test_snapshot_recv_two_chunks_assembles_and_renames);
RUN_TEST(test_snapshot_recv_size_mismatch_fails);
RUN_TEST(test_snapshot_recv_null_args_fail);
/* Task 6.4-A: 128KB snapshot in 2 chunks — full-size receive path */
RUN_TEST(test_snapshot_recv_128kb_two_chunks);
/* Task 3.5d: reconnect discards snap.part */
RUN_TEST(test_snapshot_recv_reconnect_discards_snappart);
/* Task 5.1: handle_ctrlhello connection setup only — no repl messages */