feat(controller-status): carry election time on the heartbeat (task 7.3)

The primary echoes its own last_known_elected_at_ms on every outgoing
heartbeat (threaded through xce_ha_heartbeat_req's existing scalar-arg
plumbing, alongside term/committed_lsn/committed_config_tag); a standby
processing a received heartbeat (ha_consume_heartbeat_committed, the sole
production call site) stores it unconditionally into its own
last_known_elected_at_ms, reconcile-free -- no other HA field moves.
ha_observe_heartbeat_committed itself is untouched; the new store sits
next to its call site, not inside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-16 06:03:03 +00:00
parent 21ae00b375
commit 30adc52ea3
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
9 changed files with 252 additions and 33 deletions

View file

@ -5901,12 +5901,14 @@ 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.
* committed_config_tag, elected_at_ms) it stamps onto the wire, so tests can
* assert the heartbeat carries the primary's own current committed state and
* election time. Req 1.1, 3.4, 12.2.
*/
int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term,
uint64_t committed_lsn,
uint64_t committed_config_tag) = NULL;
uint64_t committed_config_tag,
uint64_t elected_at_ms) = NULL;
/*
* ha_reconnect_connect_hook: test-only seam for the BLOCKING half of the
@ -9257,6 +9259,19 @@ void ha_consume_heartbeat_committed(cluster_t *c, const heartbeat_msg_t *hb)
ha_observe_heartbeat_committed(c, hb->committed_lsn,
hb->committed_config_tag);
/* (Req 12.2) Learn the primary's election time. Unconditional overwrite —
* no monotonic/term compare needed, unlike the committed-position observe
* above: this call site already passed the non-primary gate + stale-term
* guard above, i.e. it is already gated on "this heartbeat is from my
* currently-recognized leader," the SAME precondition
* ha_observe_heartbeat_committed's own committed_lsn/committed_config_tag
* learn relies on. Deliberately NOT added inside
* ha_observe_heartbeat_committed itself (that function's contract/tests
* are unchanged) — a separate, unconditional store immediately alongside
* it, reconcile-free (touches nothing else). */
atomic_store_explicit(&c->ha.last_known_elected_at_ms, hb->elected_at_ms,
memory_order_release);
/* 2026-07-10: drive this node's own committed_lsn + apply pump from the
* position just learned (see ha_standby_advance_committed_and_apply) —
* the commit-then-apply half of the standby ingest path. */
@ -9874,6 +9889,14 @@ static int primary_heartbeat_fn(void *arg)
uint64_t hb_committed_config_tag =
atomic_load_explicit(&ha->committed_config_tag,
memory_order_acquire);
/* Req 12.2: echo the primary's own currently-known election time
* on every heartbeat so standbys can learn it. Self-stamped by
* both promotion roads (task 7.2) at the instant this node
* committed to CTRL_PRIMARY; read unconditionally here, same
* shape as hb_term/hb_committed_lsn/hb_committed_config_tag. */
uint64_t hb_elected_at_ms =
atomic_load_explicit(&ha->last_known_elected_at_ms,
memory_order_acquire);
/* Endpoints evicted this tick; their repl deregistration runs AFTER
* ha->lock is released (repl lock-ordering — mirrors the err handlers). */
@ -9920,17 +9943,20 @@ static int primary_heartbeat_fn(void *arg)
if (ha_heartbeat_req_hook)
rc = ha_heartbeat_req_hook(ep, hb_term,
hb_committed_lsn,
hb_committed_config_tag);
hb_committed_config_tag,
hb_elected_at_ms);
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);
hb_committed_config_tag,
hb_elected_at_ms);
#else
int rc = xce_ha_heartbeat_req(ep, hb_term,
hb_committed_lsn,
hb_committed_config_tag);
hb_committed_config_tag,
hb_elected_at_ms);
#endif
if (rc < 0)
WARN("HA: heartbeat nop to standby[%d] failed: %d", i, rc);