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

@ -112,6 +112,18 @@ typedef struct {
* where votes[] is empty (n_peers=0). */
uint64_t self_lsn; /* Candidate's own WAL LSN at election start */
uint64_t self_tag; /* Candidate's own config_tag at election start */
/* ha-elect-ctx-lifecycle task 2.1: reference count governing this
* carrier's lifetime. Starts at 1 (bias reference for "currently
* published in ha->elect_ctx"), incremented once per vote_ep_ctx_t that
* captures this carrier (raft_cb_send_vote_request /
* raft_cb_send_pre_vote_request), decremented once by that endpoint's
* on_fini completion hook and once when a later round's atomic_exchange
* supersedes this carrier in ha->elect_ctx. Freed via
* ha_elect_ctx_release() when it reaches zero, from whichever of those
* two release kinds fires last — see design.md's Architecture §Mechanism
* for the full rationale (a pure per-endpoint count would allow a
* spurious free before the round is superseded). */
_Atomic int refcount;
ha_vote_entry_t votes[]; /* C99 flexible array: n_peers-1 entries */
} ha_elect_ctx_t;
@ -1709,6 +1721,14 @@ extern void (*ha_rdhup_election_hook)(ha_state_t *ha, cluster_t *c,
uint64_t my_config_tag, uint64_t my_lsn,
const char *my_name);
/*
* ha_elect_ctx_release_for_test (ha-elect-ctx-lifecycle task 2.1): drive the
* static ha_elect_ctx_release() release/free helper directly, so unit tests
* can verify the reference-count-to-zero mechanism without staging a full
* election round.
*/
void ha_elect_ctx_release_for_test(ha_elect_ctx_t *ctx);
/*
* vote_ep_ctx_set_cluster_for_test: set the cluster_t pointer inside the
* vote_ep_ctx_t stored at ep->data. Used by unit tests that need the