fix(ha): free liveness poll ctx on failed/skipped delaymgr schedule (bug-1483)

ha_arm_heartbeat_timeout_vtask published a calloc'd liveness_poll_ctx_t
into s_liveness_ctx unconditionally but only ever freed it via
liveness_poll_fn's cancelled-check path, which never runs if
delay_task_ms was never called (c==NULL, delaymgr not running) or itself
failed. Both no-schedule cases now free the ctx immediately, establishing
the invariant that s_liveness_ctx is non-NULL only when a delaymgr task
actually holds a live reference to it.

Regression test drives a real LOST PreVote round without stubbing
ha_prevote_rearm_liveness_hook (every other LOST-path test in the file
stubs it specifically to avoid this code path) and asserts the ctx isn't
left dangling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXkenK8rqE8mdS1YKjsq78
This commit is contained in:
Rongsong Shen 2026-08-02 04:36:37 +00:00
parent b17d272f1d
commit f16733ee61
2 changed files with 73 additions and 1 deletions

View file

@ -8856,6 +8856,17 @@ int primary_heartbeat_fn_for_test(cluster_t *c)
{
return primary_heartbeat_fn(c);
}
/*
* liveness_ctx_get_for_test: expose the current s_liveness_ctx pointer (as an
* opaque void*, no type leaked to callers) so a test can assert whether a
* ctx is published as "active" without a live delaymgr task actually
* servicing it (bug-1483 regression lock). Only compiled when XCE_TESTING.
*/
void *liveness_ctx_get_for_test(void)
{
return (void *)atomic_load_explicit(&s_liveness_ctx, memory_order_acquire);
}
#endif
void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c)
@ -8898,8 +8909,24 @@ void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c)
if (c && c->delaymgr.running) {
int rc = delay_task_ms(&c->delaymgr, (int)poll_ms, 1,
(delay_task_entry_fn_t)liveness_poll_fn, ctx);
if (rc < 0)
if (rc < 0) {
WARN("HA: ha_arm_heartbeat_timeout_vtask: delay_task_ms failed: %d", rc);
/* Scheduling failed -- no delaymgr task holds ctx, so
* liveness_poll_fn's cancelled-check free will never run for it.
* Free it now instead of leaving it published as "active"
* (bug-1483). */
atomic_store_explicit(&s_liveness_ctx, NULL, memory_order_release);
free(ctx);
}
} else {
/* No live delaymgr to poll on (c==NULL, e.g. unit-test mode driving
* this path directly via ha_prevote_rearm_liveness with no live
* cluster_t; or delaymgr not running). liveness_poll_fn's own
* cancelled-check free is the ONLY place a scheduled ctx ever gets
* freed, so an unscheduled ctx would otherwise leak permanently
* (bug-1483). Free it here instead. */
atomic_store_explicit(&s_liveness_ctx, NULL, memory_order_release);
free(ctx);
}
}