feat(raft-apply-decoupling): point restart replay at apply_committed_entry seam (task 2.3)

Rewire replay_wal()'s per-entry apply call from apply_wal_entry() to
apply_committed_entry() (task 2.2's shared dispatch seam), so restart
replay and the future live commit-advance pump (task 6.1) both
produce materialized state deterministically from the committed log.
apply_wal_entry() is not retired -- it remains ha.c's live standby
catch-up/replicated-apply dispatch, untouched.

Documents a real hazard: replay now fires AFTER_EVENT hooks for the
full post-checkpoint WAL range on every restart, including entries
already applied+notified in a prior process life. Traced and
confirmed inert today for both hook consumers (scheduler registers
post-boot, apiserver gates notify on a connected endpoint empty at
boot); flagged in tasks.md and in-line at the call site for
re-verification before tasks 4.1/6.1/6.2/6.3 make this path broadly
live.
This commit is contained in:
Rongsong Shen 2026-07-04 00:32:46 +00:00
parent 8c00f20cec
commit 50d016d905
5 changed files with 434 additions and 5 deletions

View file

@ -2961,10 +2961,34 @@ static int replay_wal(persist_t* p, uint64_t from_lsn,
continue;
}
/* Logical entity restore. The CRC chain + current_lsn are owned by
* this replay loop (current_lsn advanced below; last_crc at line
* ~1601), not by apply_wal_entry. */
apply_wal_entry(p, hdr.type, hdr.lsn, payload, hdr.payload_len);
/* Logical entity restore via the shared apply seam
* (raft-apply-decoupling task 2.3, design.md "Apply Seam"): the
* SAME apply_committed_entry() dispatch the live commit-advance
* apply pump will use (task 6.1), so a node's materialized state
* is the deterministic result of its committed log regardless of
* whether it got there via restart replay or live apply
* (Req 3.1-3.4). apply_wal_entry() (this replay loop's previous
* per-entry dispatch) is NOT retired by this change — it remains
* the live standby catch-up/replicated-apply dispatch used
* elsewhere (ha.c); it is simply no longer this loop's call. The
* CRC chain + current_lsn remain owned by this replay loop
* (current_lsn advanced below; last_crc at line ~1601) — neither
* dispatch function ever touched them (bug-778).
*
* KNOWN HAZARD (tracked in tasks.md Implementation Notes, task
* 2.3): this replays the FULL post-checkpoint WAL range on every
* restart (last_applied_lsn is not persisted, deliberately
* recomputed here), so apply_committed_entry()'s AFTER_EVENT
* hooks fire again for entries already applied+notified in a
* prior process life. Confirmed inert today: scheduler hooks
* aren't registered until a post-boot network JOIN
* (on_scheduler_join, sched.c), and apiserver hooks — though
* registered before this runs — gate their notify on a
* connected endpoint that is empty at first-boot replay time.
* MUST be re-verified before tasks 4.1/6.1/6.2/6.3 make this
* path broadly live. */
apply_committed_entry(p, hdr.type, hdr.lsn, payload,
hdr.payload_len);
free(payload);
/* Advance current_lsn to one past this entry */