feat(raft-ha-core-safety): establish snapshot (term,lsn) boundary on install (task 3.2)

Adds checkpointTerm (@6, append-only) to the SnapshotTransfer wire
message. The leader stamps it via persist_entry_term(p, checkpoint_lsn)
when sending a snapshot; the standby reconciles it into its own
per-LSN term index (persist_reconcile_checkpoint_term) right after
persist_load_snapshot succeeds, before applied_lsn advances. This
authoritatively overwrites any stale local term a divergent
pre-snapshot entry left at the checkpoint LSN, closing the gap task
3.1's RED test proved (Expected 7 Was 3 -> now passes). Transport
stays persist_t-free; the reconcile lives in persist.c per the
existing dependency direction. Decode is auto-generated via capnp
mapname; only the hand-rolled encode path (request.c/typemap.h, needed
for the raw snapshot payload) required a matching manual update.

test_ha_core_safety 25/0, test_config_repl 66/0, no ASAN reports.
This commit is contained in:
Rongsong Shen 2026-07-01 13:08:10 +00:00
parent 21858faba4
commit a53b9a7b6c
11 changed files with 132 additions and 23 deletions

View file

@ -2848,8 +2848,9 @@ static void test_snapshot_transfer_two_chunks(void)
xce_snapshot_chunk_hook = snaptest_hook;
snaptest_reset();
extern int ha_send_snapshot_to_standby(endpoint_t *, const char *, uint64_t);
int rc = ha_send_snapshot_to_standby(NULL, tmpdir, lsn);
extern int ha_send_snapshot_to_standby(endpoint_t *, const char *,
uint64_t, uint64_t);
int rc = ha_send_snapshot_to_standby(NULL, tmpdir, lsn, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(2, g_snaptest_chunk_count);
@ -2891,8 +2892,9 @@ static void test_snapshot_transfer_small_less_than_one_chunk(void)
xce_snapshot_chunk_hook = snaptest_hook;
snaptest_reset();
extern int ha_send_snapshot_to_standby(endpoint_t *, const char *, uint64_t);
int rc = ha_send_snapshot_to_standby(NULL, tmpdir, lsn);
extern int ha_send_snapshot_to_standby(endpoint_t *, const char *,
uint64_t, uint64_t);
int rc = ha_send_snapshot_to_standby(NULL, tmpdir, lsn, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, g_snaptest_chunk_count);

View file

@ -977,10 +977,14 @@ static void test_3_1_snapshot_install_does_not_reconcile_boundary_term(void)
"at the STALE local term");
/* A leader at a DIFFERENT (higher) term ships a checkpoint whose position
* IS this exact boundary LSN. Today's on-disk/wire format carries no
* term field at all, so the legacy (position-only) shape is exactly what
* a real checkpoint transfer installs via ha_handle_snapshot_chunk's real
* chunk-assembly + rename + load path (not a stub). */
* IS this exact boundary LSN. The on-disk snapshot payload itself stays
* legacy (position-only: magic + lsn, no term) -- exactly what a real
* checkpoint transfer installs via ha_handle_snapshot_chunk's real
* chunk-assembly + rename + load path (not a stub). The wire chunk's
* checkpoint_term field (task 3.2) is what the leader now stamps with its
* own term for this checkpoint (via persist_entry_term at send time,
* ha_send_snapshot_to_standby) -- set here to simulate that real sender
* behavior, independent of the (still-legacy) snapshot file format. */
char snap_body[16];
memcpy(snap_body, "XCESNAP\0", 8);
uint64_t boundary_lsn_u64 = (uint64_t)BOUNDARY_LSN;
@ -988,11 +992,12 @@ static void test_3_1_snapshot_install_does_not_reconcile_boundary_term(void)
snapshot_transfer_t chunk;
memset(&chunk, 0, sizeof(chunk));
chunk.snapshot_lsn = boundary_lsn_u64;
chunk.chunk_index = 0;
chunk.chunk_offset = 0;
chunk.total_size = sizeof(snap_body);
chunk.is_last = 1;
chunk.snapshot_lsn = boundary_lsn_u64;
chunk.chunk_index = 0;
chunk.chunk_offset = 0;
chunk.total_size = sizeof(snap_body);
chunk.is_last = 1;
chunk.checkpoint_term = (uint64_t)CHECKPOINT_TERM;
int rc = ha_handle_snapshot_chunk(&ha, m.p, m.p->snap_path, &chunk,
(const uint8_t*)snap_body,