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:
parent
58e047f533
commit
6529dcf08f
14 changed files with 879 additions and 22 deletions
|
|
@ -36,6 +36,7 @@ extern void test_cluster(void);
|
|||
extern void test_cluster_parse(void);
|
||||
extern void test_libxce_retry(void);
|
||||
extern void test_netio_on_connected(void);
|
||||
extern void test_netio_on_fini(void);
|
||||
extern void test_netio_arm_failure(void);
|
||||
extern void test_netio_close_propagation(void);
|
||||
extern void test_netio_bounded_connect(void);
|
||||
|
|
@ -149,6 +150,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_cluster_parse);
|
||||
TEST(test_libxce_retry);
|
||||
TEST(test_netio_on_connected);
|
||||
TEST(test_netio_on_fini);
|
||||
TEST(test_netio_arm_failure);
|
||||
TEST(test_netio_close_propagation);
|
||||
TEST(test_netio_bounded_connect);
|
||||
|
|
|
|||
169
src/lib/testsuite/test_netio_on_fini.c
Normal file
169
src/lib/testsuite/test_netio_on_fini.c
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* test_netio_on_fini.c — TDD tests for on_fini completion hook in endpoint_t.
|
||||
*
|
||||
* Task 1.1: ha-elect-ctx-lifecycle spec
|
||||
* Boundary: netio.c / netio.h
|
||||
*
|
||||
* Mirrors test_netio_on_connected.c's structure for the completion-side
|
||||
* counterpart hook. Tests verify:
|
||||
* 1. on_fini field exists in endpoint_t (compile-time check), NULL by default
|
||||
* 2. endpoint_set_on_fini() stores the callback
|
||||
* 3. Hook fires exactly once from endpoint_fini(), before ep->data is freed
|
||||
* 4. ep->data is freed (NULL'd) after endpoint_fini() returns, as before
|
||||
* 5. No callback set — endpoint_fini() still frees ep->data, no crash
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
#include "netio.h"
|
||||
|
||||
/* ── Static state for callback tracking ──────────────────────────── */
|
||||
|
||||
static int g_fini_cb_count = 0;
|
||||
static endpoint_t *g_fini_cb_ep = NULL;
|
||||
static void *g_fini_cb_data_seen = NULL;
|
||||
|
||||
static void on_fini_stub(endpoint_t *ep)
|
||||
{
|
||||
g_fini_cb_count++;
|
||||
g_fini_cb_ep = ep;
|
||||
/* Capture ep->data as observed INSIDE the hook — must equal the
|
||||
* original allocation, proving it is still valid (not yet freed) at
|
||||
* the moment the hook runs. */
|
||||
g_fini_cb_data_seen = ep->data;
|
||||
}
|
||||
|
||||
static void reset_cb_state(void)
|
||||
{
|
||||
g_fini_cb_count = 0;
|
||||
g_fini_cb_ep = NULL;
|
||||
g_fini_cb_data_seen = NULL;
|
||||
}
|
||||
|
||||
/* ── Test 1: Field exists and is zero-initialised after endpoint_get ── */
|
||||
|
||||
static void test_on_fini_field_exists_and_is_null(void)
|
||||
{
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
TEST_ASSERT_NULL(ep->on_fini);
|
||||
|
||||
free(ep);
|
||||
printf("test_on_fini_field_exists_and_is_null: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 2: endpoint_set_on_fini stores the callback ────────────── */
|
||||
|
||||
static void test_endpoint_set_on_fini_stores_callback(void)
|
||||
{
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
int rc = endpoint_set_on_fini(ep, on_fini_stub);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_PTR(on_fini_stub, ep->on_fini);
|
||||
|
||||
free(ep);
|
||||
printf("test_endpoint_set_on_fini_stores_callback: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 3: endpoint_set_on_fini with NULL ep returns -EINVAL ───── */
|
||||
|
||||
static void test_endpoint_set_on_fini_null_ep(void)
|
||||
{
|
||||
int rc = endpoint_set_on_fini(NULL, on_fini_stub);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
printf("test_endpoint_set_on_fini_null_ep: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 4: endpoint_set_on_fini accepts NULL callback ──────────── */
|
||||
|
||||
static void test_endpoint_set_on_fini_null_cb(void)
|
||||
{
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
int rc = endpoint_set_on_fini(ep, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NULL(ep->on_fini);
|
||||
|
||||
free(ep);
|
||||
printf("test_endpoint_set_on_fini_null_cb: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* ── Test 5: hook fires exactly once from endpoint_fini(), and ep->data
|
||||
* is still valid (not yet freed) at the moment the hook runs ──────
|
||||
*/
|
||||
static void test_on_fini_fires_once_before_data_freed(void)
|
||||
{
|
||||
reset_cb_state();
|
||||
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
void *marker = malloc(16);
|
||||
TEST_ASSERT_NOT_NULL(marker);
|
||||
ep->data = marker;
|
||||
|
||||
endpoint_set_on_fini(ep, on_fini_stub);
|
||||
TEST_ASSERT_EQUAL_PTR(on_fini_stub, ep->on_fini);
|
||||
|
||||
endpoint_fini(ep);
|
||||
|
||||
/* Hook fired exactly once, with the correct endpoint. */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_fini_cb_count);
|
||||
TEST_ASSERT_EQUAL_PTR(ep, g_fini_cb_ep);
|
||||
|
||||
/* ep->data was still the original allocation AT HOOK-CALL TIME —
|
||||
* proves the hook observes teardown completion before the generic
|
||||
* FREEUP(ep->data) reclaims it. */
|
||||
TEST_ASSERT_EQUAL_PTR(marker, g_fini_cb_data_seen);
|
||||
|
||||
/* After endpoint_fini() returns, ep->data is freed and NULL'd —
|
||||
* existing FREEUP(ep->data) behavior is unchanged. */
|
||||
TEST_ASSERT_NULL(ep->data);
|
||||
|
||||
free(ep);
|
||||
printf("test_on_fini_fires_once_before_data_freed: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 6: No callback set — endpoint_fini still frees data, no crash ── */
|
||||
|
||||
static void test_no_on_fini_callback_data_still_freed_no_crash(void)
|
||||
{
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
void *marker = malloc(16);
|
||||
TEST_ASSERT_NOT_NULL(marker);
|
||||
ep->data = marker;
|
||||
|
||||
/* No on_fini set. */
|
||||
endpoint_fini(ep);
|
||||
|
||||
TEST_ASSERT_NULL(ep->data);
|
||||
|
||||
free(ep);
|
||||
printf("test_no_on_fini_callback_data_still_freed_no_crash: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test suite entry point ───────────────────────────────────────── */
|
||||
|
||||
void test_netio_on_fini(void)
|
||||
{
|
||||
printf("\n>> netio on_fini completion hook tests\n\n");
|
||||
|
||||
RUN_TEST(test_on_fini_field_exists_and_is_null);
|
||||
RUN_TEST(test_endpoint_set_on_fini_stores_callback);
|
||||
RUN_TEST(test_endpoint_set_on_fini_null_ep);
|
||||
RUN_TEST(test_endpoint_set_on_fini_null_cb);
|
||||
RUN_TEST(test_on_fini_fires_once_before_data_freed);
|
||||
RUN_TEST(test_no_on_fini_callback_data_still_freed_no_crash);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue