feat(ha): reference-counted ha_elect_ctx_t lifecycle (ha-elect-ctx-lifecycle)

Replace the deliberate election-carrier leak with an atomic refcount:
a bias reference held by ha->elect_ctx plus one reference per captured
vote endpoint, released via a new endpoint_t.on_fini teardown-completion
hook (fired once, before ep->data is freed). Both real-election and
PreVote capture sites (raft_cb_send_vote_request,
raft_cb_send_pre_vote_request) increment on capture; both publish sites
(ha_start_election_async, ha_start_prevote_async) now atomic_exchange
the outgoing carrier and release it instead of leaking or freeing
outright, so a still-outstanding vote endpoint keeps the superseded
carrier alive until its own teardown completes.

Verified: 758+ controller tests green, isolated LeakSanitizer run shows
zero ha_elect_ctx_t leaks across many election/PreVote rounds, no
premature-free/UAF under supersession, and a live 10-round HA soak
(36 jobs, zero loss, all 3 nodes served as primary).
This commit is contained in:
Rongsong Shen 2026-07-12 04:25:42 +00:00
parent 58e047f533
commit 6529dcf08f
14 changed files with 879 additions and 22 deletions

View file

@ -955,6 +955,19 @@ int endpoint_set_on_connected(endpoint_t* ep, void (*cb)(endpoint_t*))
return 0;
}
int endpoint_set_on_fini(endpoint_t* ep, void (*cb)(endpoint_t*))
{
if (ep == NULL) {
return -EINVAL;
}
mylock_lock(&(ep->lock));
ep->on_fini = cb;
mylock_unlock(&(ep->lock));
return 0;
}
int endpoint_enq(endpoint_t* ep, data_item_t* it)
{
int rc = 0;
@ -1091,6 +1104,15 @@ int endpoint_is_tearing_down(endpoint_t* ep)
void endpoint_fini(endpoint_t* ep)
{
dataq_fini(&(ep->outq));
/* ha-elect-ctx-lifecycle task 1.1: fire the completion hook (if set)
* immediately before ep->data is freed, so a consumer can observe
* teardown completion while ep->data is still valid. Does not change
* teardown ordering/timing — same free happens right after, unconditionally. */
if (ep->on_fini) {
ep->on_fini(ep);
}
FREEUP(ep->data);
if (ep->fd >= 0) {