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:
parent
c45f80ab2b
commit
47f733891b
3 changed files with 16 additions and 10 deletions
|
|
@ -508,12 +508,12 @@ static int myreq_handler(vtask_t* task)
|
||||||
/* Distinguish the denial reason for operator diagnosis
|
/* Distinguish the denial reason for operator diagnosis
|
||||||
* (Req 7.2). raft_on_pre_vote_request denies on (a) stale term
|
* (Req 7.2). raft_on_pre_vote_request denies on (a) stale term
|
||||||
* — cand_term < raft.term — or (b) the shared eligibility gate
|
* — cand_term < raft.term — or (b) the shared eligibility gate
|
||||||
* (raft_cb_vote_eligible): PRIMARY-deny, fresh-heartbeat
|
* (raft_cb_vote_eligible). Mirror that gate's exact predicate
|
||||||
* stickiness, or catch-up. We re-derive which fired WITHOUT
|
* ORDER so the logged reason cannot disagree with the actual
|
||||||
* calling the file-static heartbeat-fresh predicate: when the
|
* denial: stale-term, then PRIMARY, then fresh-heartbeat on a
|
||||||
* role is STANDBY and the node is neither catching up nor at a
|
* STANDBY, then catch-up. ha_heartbeat_is_fresh is a pure read
|
||||||
* stale term, the only remaining eligibility denial is fresh
|
* exposed for this purpose, so the fresh-heartbeat-vs-catch-up
|
||||||
* heartbeat (a stale heartbeat would have passed the gate). */
|
* overlap is reported exactly as the gate decided it. */
|
||||||
uint64_t my_raft_term = c->ha.raft.term;
|
uint64_t my_raft_term = c->ha.raft.term;
|
||||||
if (cand_term < my_raft_term) {
|
if (cand_term < my_raft_term) {
|
||||||
deny_reason = "stale_term";
|
deny_reason = "stale_term";
|
||||||
|
|
@ -522,11 +522,12 @@ static int myreq_handler(vtask_t* task)
|
||||||
&c->ha.role, memory_order_acquire);
|
&c->ha.role, memory_order_acquire);
|
||||||
if (role == CTRL_PRIMARY)
|
if (role == CTRL_PRIMARY)
|
||||||
deny_reason = "primary";
|
deny_reason = "primary";
|
||||||
|
else if (role == CTRL_STANDBY &&
|
||||||
|
ha_heartbeat_is_fresh(&c->ha))
|
||||||
|
deny_reason = "fresh_heartbeat";
|
||||||
else if (atomic_load_explicit(&c->ha.in_catch_up,
|
else if (atomic_load_explicit(&c->ha.in_catch_up,
|
||||||
memory_order_acquire))
|
memory_order_acquire))
|
||||||
deny_reason = "catch_up";
|
deny_reason = "catch_up";
|
||||||
else if (role == CTRL_STANDBY)
|
|
||||||
deny_reason = "fresh_heartbeat";
|
|
||||||
else
|
else
|
||||||
deny_reason = "eligibility";
|
deny_reason = "eligibility";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ static int vote_ep_err_handler(void *ep_raw);
|
||||||
* PRIMARY registers the persist hooks and arms the own-term gate. */
|
* PRIMARY registers the persist hooks and arms the own-term gate. */
|
||||||
static void ha_register_persist_hooks(cluster_t *c);
|
static void ha_register_persist_hooks(cluster_t *c);
|
||||||
static void ha_arm_own_term_gate(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 int primary_heartbeat_fn(void *arg);
|
||||||
static void on_standby_connected(endpoint_t *ep);
|
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
|
* Uses CLOCK_MONOTONIC seconds to avoid wall-clock skew; whole-second
|
||||||
* granularity is sufficient for the 10 s default timeout.
|
* 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 */
|
time_t last = ha->last_activity; /* time_t load is atomic on LP64 */
|
||||||
if (last == 0)
|
if (last == 0)
|
||||||
|
|
|
||||||
|
|
@ -763,6 +763,11 @@ void ha_discover_primary(ha_state_t *ha, cluster_t *c);
|
||||||
/* ha_on_heartbeat_received: reset the heartbeat deadline (Req 6.3). */
|
/* ha_on_heartbeat_received: reset the heartbeat deadline (Req 6.3). */
|
||||||
void ha_on_heartbeat_received(ha_state_t *ha);
|
void ha_on_heartbeat_received(ha_state_t *ha);
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
|
int ha_heartbeat_is_fresh(ha_state_t *ha);
|
||||||
|
|
||||||
/* ha_disarm_heartbeat_timeout_vtask: stop the heartbeat watchdog thread. */
|
/* ha_disarm_heartbeat_timeout_vtask: stop the heartbeat watchdog thread. */
|
||||||
void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha);
|
void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue