feat(ha-heartbeat-committed-propagation): stamp own committed+term on primary heartbeat (task 3.1)

primary_heartbeat_fn now sends xce_ha_heartbeat_req with the primary's OWN
atomic ha->term / persist.committed_lsn / committed_config_tag (NOT last_known_*),
snapshotted once per tick (Req 1.1, 3.4). XCE_TESTING seam adds a capturing
ha_heartbeat_req_hook (precedence new->legacy->real) so existing ha_nop_req_hook
liveness tests stay green; send-failure WARN + last_push_time unchanged. New test
poisons last_known_* to prove own-committed. Direct binary 119/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-20 12:22:50 +00:00
parent 90e7179a89
commit d534f83e46
4 changed files with 158 additions and 3 deletions

View file

@ -2764,6 +2764,17 @@ int (*ha_push_config_hook)(void *c) = NULL;
*/
int (*ha_nop_req_hook)(endpoint_t *ep) = NULL;
/*
* ha_heartbeat_req_hook: test-only seam for the HA heartbeat send in
* primary_heartbeat_fn. When non-NULL, primary_heartbeat_fn calls this INSTEAD
* OF xce_ha_heartbeat_req with the SAME (term, committed_lsn,
* committed_config_tag) it stamps onto the wire, so tests can assert the
* heartbeat carries the primary's own current committed state. Req 1.1, 3.4.
*/
int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term,
uint64_t committed_lsn,
uint64_t committed_config_tag) = NULL;
/*
* ha_repl_push_config_for_standby_hook: test-only seam for the per-standby
* config push triggered by the heartbeat-reply decision loop (Task 3.3).
@ -5286,6 +5297,20 @@ static int primary_heartbeat_fn(void *arg)
if (timeout_s == 0)
timeout_s = HA_DEFAULT_HEARTBEAT_TIMEOUT_MS / 1000;
/* Stamp the primary's OWN current committed position + term onto
* every heartbeat this tick (Req 1.1, 3.4). Read atomically here,
* once per tick, so every standby sees a consistent snapshot. Use
* the primary's authoritative state — NOT the standby-side
* last_known_* fields. */
uint64_t hb_term =
atomic_load_explicit(&ha->term, memory_order_acquire);
uint64_t hb_committed_lsn =
atomic_load_explicit(&c->persist.committed_lsn,
memory_order_acquire);
uint64_t hb_committed_config_tag =
atomic_load_explicit(&ha->committed_config_tag,
memory_order_acquire);
/* Endpoints evicted this tick; their repl deregistration runs AFTER
* ha->lock is released (repl lock-ordering — mirrors the err handlers). */
endpoint_t *evicted[MAX_HA_CANDIDATES];
@ -5324,10 +5349,24 @@ static int primary_heartbeat_fn(void *arg)
time_t elapsed = now - sctx->last_push_time;
if (elapsed >= (time_t)interval_s) {
/* Heartbeat carries the primary's own committed + term
* (Req 1.1, 3.4). */
#ifdef XCE_TESTING
int rc = ha_nop_req_hook ? ha_nop_req_hook(ep) : xce_nop_req(ep);
int rc;
if (ha_heartbeat_req_hook)
rc = ha_heartbeat_req_hook(ep, hb_term,
hb_committed_lsn,
hb_committed_config_tag);
else if (ha_nop_req_hook)
rc = ha_nop_req_hook(ep);
else
rc = xce_ha_heartbeat_req(ep, hb_term,
hb_committed_lsn,
hb_committed_config_tag);
#else
int rc = xce_nop_req(ep);
int rc = xce_ha_heartbeat_req(ep, hb_term,
hb_committed_lsn,
hb_committed_config_tag);
#endif
if (rc < 0)
WARN("HA: heartbeat nop to standby[%d] failed: %d", i, rc);