docs(raft-ha-core-safety): document term-blind bypass safe-by-guard + pin restart identity (task 6.2)

The term-blind ha_apply_single_entry direct-persist branch (carried
forward from task 2.4) is unreachable with a real non-zero term in any
current production path: its only trigger, raft_send_append_entries,
has zero production call sites (library simulator only). Both real
senders (repl_broadcast, ha_stream_wal_to_standby) always emit a
non-zero term for a present LSN. Documents this in ha.c and design.md
rather than structurally retiring a dormant path (YAGNI), with an
explicit flag for future re-evaluation if a heartbeat-driver task ever
wires up raft_send_append_entries.

Adds two regression pins: restart preserves (term,lsn) identity
(replay_wal already reconstructs the term index via record_entry_term),
and real senders never produce the term-blind wire shape.

test_ha_core_safety 28/0, no regression, no ASAN reports.
This commit is contained in:
Rongsong Shen 2026-07-01 13:55:06 +00:00
parent 4e1e3e07fb
commit 9344b8ed60
4 changed files with 338 additions and 8 deletions

View file

@ -2130,7 +2130,32 @@ static void ha_advance_applied_lsn(ha_state_t *ha, uint64_t new_lsn)
* Payload bytes (raw_payload/raw_payload_len, extracted by xce_decode)
* are passed through to apply_wal_entry so entity state is restored —
* not just the LSN chain (task 7.3).
* Returns 0 on success, non-zero on error (caller logs and skips). */
* Returns 0 on success, non-zero on error (caller logs and skips).
*
* raft-ha-core-safety task 6.2 — TERM-BLIND DIRECT-PERSIST BYPASS, SAFE-BY-
* GUARD (documented, not structurally retired): this function does NOT go
* through raft_on_append_entries_request's logOk/(term,lsn) reconcile — it
* calls persist_wal_write_replicated directly. Task 2.4 deferred deciding
* this bypass's fate to 6.2; 6.2's reachability trace (see the two call
* sites below and ha_apply_wal_catchup_entry) found it is UNREACHABLE with
* real production data today: every real sender (repl_broadcast for live
* replication; ha_stream_wal_to_standby for catch-up) always supplies
* n_entries>=1 with a non-zero term (persist_entry_term's legacy-zero rule
* maps a stored term of 0 to WAL_LEGACY_TERM for any present LSN — it never
* returns 0 for a real entry). The ONLY function that could ever drive a
* term==0 / n_entries==0 message into this path (repl_send_append_entries)
* is called solely from the raft library's raft_send_append_entries, which
* has zero production callers (only lib/raft/raft_sim.c and an XCE_TESTING
* helper call it) — so that trigger is dormant, not live, code. Even if this
* path were reached, the task-2.2 (term,lsn,payload-crc)-aware guard inside
* persist_wal_write_replicated already refuses to degrade a stored non-empty
* entry to empty and raises the sticky non-promotable fence on an
* unrepairable below-tip conflict — pinned by
* test_2_3_termblind_route_payload_guarded and
* test_2_3_unrepairable_catchup_raises_nonpromotable_fence
* (test_ha_core_safety.c). See ha_apply_wal_catchup_entry below and
* test_6_2_real_senders_never_produce_termblind_shape for the reachability
* evidence this comment summarizes. */
static int ha_apply_single_entry(persist_t *p, wal_catchup_entry_t *e)
{
/* Persist-before-apply-before-ACK: durably write the replicated entry to
@ -2316,7 +2341,18 @@ int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
* retires the term-0 direct-apply path for live/catch-up replication.
* Entries stream in ascending LSN order, so the library's contiguity
* (prev tuple) is satisfied in sequence; a not-yet-applicable entry
* (logOk reject) is left for the catch-up continuation. */
* (logOk reject) is left for the catch-up continuation.
*
* raft-ha-core-safety task 6.2: the `sub->term != 0` fallthrough
* below is the term-blind direct-persist bypass (ha_apply_single_
* entry) — documented SAFE-BY-GUARD, not structurally retired.
* ha_stream_wal_to_standby (the only real batch-catch-up sender)
* always stamps sub->term via persist_entry_term, which maps a
* legacy on-disk term of 0 to WAL_LEGACY_TERM for any present LSN,
* so a real streamed sub-entry is never term==0 — this fallthrough
* is unreachable with production data (see the ha_apply_single_
* entry comment for the full reachability trace + the pin test
* test_6_2_real_senders_never_produce_termblind_shape). */
if (c && c->ha.raft_c == c && sub->term != 0) {
if (ha_apply_term_entry_via_library(ha, p, c, ep, sub)) {
last_good_lsn = sub->lsn;
@ -2356,7 +2392,20 @@ int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
return 0;
}
/* ── Single-entry mode (task 2.1 backward-compatible path) ────────────── */
/* ── Single-entry mode (task 2.1 backward-compatible path) ──────────────
* raft-ha-core-safety task 6.2: this branch calls ha_apply_single_entry
* UNCONDITIONALLY — no term check at all — so it is the other half of the
* term-blind bypass documented at ha_apply_single_entry above. It is only
* ever reached with entry->n_entries==0, the 0-entry AppendEntries
* heartbeat shape built by repl_send_append_entries. That function is
* called solely from the raft library's raft_send_append_entries, which
* has ZERO production call sites (only lib/raft/raft_sim.c and an
* XCE_TESTING test helper call it) — so in the CURRENT codebase this
* branch is dormant/unreachable from any live sender, not a legacy-
* rollout shape carrying real data. Documented SAFE-BY-GUARD (not
* structurally retired): see the ha_apply_single_entry comment for the
* full reachability trace and the task-2.2 guard that would still bound
* the damage if this ever became reachable. */
int rc = ha_apply_single_entry(p, entry);
if (rc != 0) {
/* -EAGAIN = contiguity gap (bug-850): dropped quietly, backfilled by