feat(raft-apply-decoupling): route DIVERGENT verdict through existing fence (task 5.2)

Add bool apply_divergent to the existing recover_policy_inputs_t;
persist_recover_decision() unconditionally short-circuits to the
existing RECOVER_DECISION_FENCE_UNRECOVERABLE when true, reusing the
same sticky recovery_unrecoverable flag and
persist_recover_loud_fence_if_unrecoverable() -- no new fence
mechanism, no ha.c changes needed (the existing promotion gate
already reads the flag independently), no new self-termination path.

Fixes a real bug found during review: the fence's operator-facing
ERROR() log unconditionally asserted a "no healthy peer reachable...
durable_floor_lsn < hwm" clause that was only truthful for the
pre-existing structural-fence trigger. For the new apply_divergent
trigger this produced literally false claims in test output
("100 < 100", "2 < 0"). Fixed by branching the log call itself on
apply_divergent so it states only the verified reason.
This commit is contained in:
Rongsong Shen 2026-07-04 12:22:09 +00:00
parent c005d40741
commit 099ee1a216
4 changed files with 417 additions and 24 deletions

View file

@ -85,6 +85,21 @@ typedef struct {
* when a catch-up was attempted) */
uint64_t durable_floor_lsn; /* highest intact replayed LSN (truncation boundary) */
uint64_t cluster_hwm_lsn; /* cluster committed high-water mark, for logging */
/* raft-apply-decoupling task 5.2 (Req 8.3/8.4): the post-replay
* apply_verdict_t computed a semantic mismatch (APPLY_DIVERGENT) — the
* materialized state does not match the committed log tip even though
* the WAL bytes replayed cleanly. This is a NEW INPUT to the existing
* Boot Policy Decision Table, not a new fence mechanism: unlike a
* structurally truncated/behind log (which a healthy peer's missing
* entries can repair), an apply-time defect is not something streaming
* more entries from a peer proves fixed, so persist_recover_decision()
* treats this field as an UNCONDITIONAL fence trigger — set it true and
* every other field in this struct becomes irrelevant to the outcome
* (see persist_recover_decision's implementation). Defaults to false
* (an ordinary structural-only evaluation) when left unset by a
* designated initializer, matching every pre-existing call site. */
bool apply_divergent;
} recover_policy_inputs_t;
/* ============================================================
@ -108,9 +123,11 @@ typedef enum {
* expectation (Req 8.2). */
APPLY_DIVERGENT = 1, /* materialized state does NOT match the log-derived
* expectation (Req 8.3) — a hidden apply defect.
* Routing this into the existing non-promotable
* fence mechanism is raft-apply-decoupling task 5.2;
* this type only classifies, it does not fence. */
* Routed into the existing non-promotable loud-fence
* mechanism via recover_policy_inputs_t.apply_divergent
* (raft-apply-decoupling task 5.2, Req 8.4) — this
* type itself still only classifies; persist_recover()
* is what acts on it. */
} apply_verdict_t;
/* Lightweight post-replay expectation, derived purely from scanning the
@ -340,8 +357,9 @@ struct persist_s {
* defaults to APPLY_CLEAN before recovery runs, mirroring
* recovery_verdict's default-clean convention. Written single-threaded
* during recovery; read by persist_apply_verdict/persist_last_applied_lsn
* thereafter (task 5.2 will additionally consume apply_verdict to route a
* DIVERGENT result into the existing loud-fence mechanism). */
* thereafter. A DIVERGENT result is additionally routed by persist_recover()
* into the existing loud-fence mechanism (task 5.2, Req 8.4) via
* recover_policy_inputs_t.apply_divergent. */
_Atomic uint64_t last_applied_lsn;
apply_verdict_t apply_verdict;