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

@ -2606,6 +2606,21 @@ int ha_handle_snapshot_chunk(ha_state_t *ha, persist_t *p,
return rc;
}
/* raft-ha-core-safety task 3.2 (Req 3.1/5.1) — Snapshot Boundary
* Reconcile: authoritatively overwrite the per-LSN term index at the
* checkpoint LSN with the checkpoint's own term (as carried on the
* wire). Establishes a verifiable (term, lsn) boundary so a divergent
* pre-snapshot entry's STALE local term at this exact LSN is reconciled
* rather than silently surviving the install. Applies identically to
* both a legacy (position-only) and an entity-bearing checkpoint — the
* term reconcile is orthogonal to entity content. chunk->checkpoint_term
* == 0 means a legacy sender never stamped the wire field; persist's
* existing legacy-0 -> WAL_LEGACY_TERM convention (applied uniformly at
* read-back by persist_entry_term) already covers that case, so no
* special-casing is needed here. */
persist_reconcile_checkpoint_term(p, chunk->snapshot_lsn,
chunk->checkpoint_term);
/* Advance applied_lsn to checkpoint_lsn (in-memory only — Req 4.2) */
atomic_store_explicit(&ha->applied_lsn, p->checkpoint_lsn,
memory_order_release);
@ -2753,7 +2768,7 @@ static void ha_snapshot_do_send(cluster_t *c, endpoint_t *ep, int peer_idx)
#endif
if (c && c->persist.snap_path[0]) {
extern int ha_send_snapshot_to_standby(endpoint_t *, const char *,
uint64_t);
uint64_t, uint64_t);
/* Req 4.1: create a FRESH checkpoint so the transferred snapshot
* reflects current state (also trims WAL + rotates old snaps).
* -EBUSY means another checkpoint is in flight — fall back to the
@ -2763,8 +2778,14 @@ static void ha_snapshot_do_send(cluster_t *c, endpoint_t *ep, int peer_idx)
if (ckpt_rc != 0 && ckpt_rc != -EBUSY)
WARN("HA repl: persist_checkpoint before snapshot send "
"failed rc=%d — sending existing checkpoint", ckpt_rc);
/* raft-ha-core-safety task 3.2: source the checkpoint's own term so
* the standby can reconcile its per-LSN term index at the boundary
* after install (Snapshot Boundary Reconcile). */
uint64_t checkpoint_term =
persist_entry_term(&c->persist, c->persist.checkpoint_lsn);
ha_send_snapshot_to_standby(ep, c->persist.snap_path,
c->persist.checkpoint_lsn);
c->persist.checkpoint_lsn,
checkpoint_term);
}
}