fix(ha-prevote): make PreVote denial log-reason match the eligibility gate order exactly

The cluster.c denial-reason elimination ordered catch_up before fresh_heartbeat,
disagreeing with raft_cb_vote_eligible (which checks fresh-heartbeat-on-STANDBY
before catch-up). In the rare STANDBY catch-up + fresh-heartbeat overlap the log
could report catch_up while the gate denied on fresh_heartbeat. Expose the pure
read predicate ha_heartbeat_is_fresh and mirror the gate's exact order so the
logged reason can no longer disagree with the actual denial. Decision unchanged;
log-string fidelity only. prevote/vote-gate/failover-prevote suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-16 07:48:50 +00:00
parent c45f80ab2b
commit 47f733891b
3 changed files with 16 additions and 10 deletions

View file

@ -87,7 +87,7 @@ static int vote_ep_err_handler(void *ep_raw);
* PRIMARY registers the persist hooks and arms the own-term gate. */
static void ha_register_persist_hooks(cluster_t *c);
static void ha_arm_own_term_gate(cluster_t *c);
static int ha_heartbeat_is_fresh(ha_state_t *ha);
int ha_heartbeat_is_fresh(ha_state_t *ha); /* exposed for PreVote denial-reason logging (cluster.c) */
static int primary_heartbeat_fn(void *arg);
static void on_standby_connected(endpoint_t *ep);
@ -2093,7 +2093,7 @@ int ha_restore_term(ha_state_t *ha, const char *workdir)
* Uses CLOCK_MONOTONIC seconds to avoid wall-clock skew; whole-second
* granularity is sufficient for the 10 s default timeout.
*/
static int ha_heartbeat_is_fresh(ha_state_t *ha)
int ha_heartbeat_is_fresh(ha_state_t *ha)
{
time_t last = ha->last_activity; /* time_t load is atomic on LP64 */
if (last == 0)