feat(ha-heartbeat-committed-propagation): standby OP_nop consume branch + term-guard (task 2.2)

Append a committed-consume branch to the OP_nop handler (existing liveness/reply
byte-unchanged): ha_consume_heartbeat_committed applies the non-primary gate
(3.4) + stale-term guard (accept hb->term >= ha->term, 3.2), then delegates to
the monotonic ha_observe_heartbeat_committed (1.2). Absent body / zero / failed
gate are no-ops (4.2). No vote-floor edit — safety is the term-guard. 4 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-20 12:02:15 +00:00
parent 8bfa861be2
commit 90e7179a89
5 changed files with 200 additions and 1 deletions

View file

@ -899,6 +899,24 @@ void ha_on_heartbeat_received(ha_state_t *ha);
void ha_observe_heartbeat_committed(cluster_t *c, uint64_t committed_lsn,
uint64_t committed_config_tag);
/*
* ha_consume_heartbeat_committed: the standby-side OP_nop consume branch
* (ha-heartbeat-committed-propagation, Req 1.2, 3.2, 3.4, 4.2).
*
* Safe to call unconditionally from the OP_nop handler with the decoded
* heartbeatmsg body. It applies the two safety gates before delegating to the
* monotonic ha_observe_heartbeat_committed:
* - non-primary gate (3.4): a primary uses its own authoritative committed and
* never consumes an inbound value;
* - term-guard (3.2): a heartbeat whose advertised term is behind this node's
* term is rejected, fencing a stale/partitioned primary from inflating the
* vote-freshness floor.
* NULL body, a failed gate, or a zero committed value updates nothing (4.2);
* the helper never touches liveness or any other OP_nop behaviour (4.1 — the
* caller invokes this AFTER the unchanged liveness/reply path).
*/
void ha_consume_heartbeat_committed(cluster_t *c, const heartbeat_msg_t *hb);
/* ha_heartbeat_is_fresh: 1 if a primary message arrived within
* heartbeat_timeout_ms (leader-stickiness predicate). Exposed for PreVote
* denial-reason logging in cluster.c; pure read, no state change. */