forked from lei.chen/myxce
feat(raft-drive-async): resolve inbound/outbound channel ownership on reconnect (task 2.2)
When a reconnect registers a new outbound control channel for peer X, on_standby_connected now evicts any pre-existing standby_endpoints entry for the same peer_idx (a promoted vote-ep or a stale prior channel), keeping exactly one live outbound channel per peer (Req 2.6/4.4). The stale ep is torn down OUTSIDE ha->lock via standby_ep_err_handler_primary (which re-takes the lock) and reclaimed; resolution is strictly per-peer. Also fixes endpoint leaks on the endpoint_get() calloc paths: every endpoint_fini() error path now free(ep) (fini reclaims inner members only), closing the task-2.1 GREEN-test leak masked by detect_leaks=0. Tests 18-19 in test_ha_liveness.c assert single-survivor resolution and per-peer isolation. 196/196 controller unit tests pass.
This commit is contained in:
parent
e07667ff9b
commit
e03ab9bd72
2 changed files with 259 additions and 8 deletions
|
|
@ -1118,6 +1118,162 @@ static void test_reconnect_offload_dedups_per_peer(void)
|
|||
printf("test_reconnect_offload_dedups_per_peer: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Tests 18–19: raft-drive-async Task 2.2 — inbound/outbound ownership ─────
|
||||
*
|
||||
* Acceptance criteria (Req 2.6, 4.4; design ReconnectOffload race resolution,
|
||||
* ha-async-io.md connection ownership):
|
||||
* - When a reconnect succeeds and registers a NEW outbound control channel
|
||||
* for peer X, any PRE-EXISTING standby_endpoints entry for the SAME peer X
|
||||
* (a promoted vote-ep or a stale prior channel that has not yet been torn
|
||||
* down) must be resolved deterministically: exactly ONE live outbound
|
||||
* channel per peer survives — no dual live connection, no orphaned/leaked
|
||||
* endpoint, and the survivor is the freshly-connected one so no queued
|
||||
* message is delivered down a dead duplicate (lost/reordered, Req 2.6).
|
||||
*
|
||||
* The collision: BOTH raft_cb_become_leader (promotes vote eps) and
|
||||
* on_standby_connected (late-joiner reconnect) zarray_store into
|
||||
* standby_endpoints keyed only by ep-POINTER identity — never by peer_idx.
|
||||
* So two distinct eps for the same peer can both register → two state-push
|
||||
* fan-out targets → exactly the bug-851 dual-channel hazard.
|
||||
*
|
||||
* Determinism: this is the ownership-DECISION logic (not a real socket-timing
|
||||
* race); the real concurrent inbound-accept-vs-outbound-connect timing race is
|
||||
* covered by the partition soak (task 4.1). Here we drive the registration
|
||||
* point directly with two heap endpoints for the same peer_idx and assert the
|
||||
* resolution. Heap eps + endpoint_init make the teardown production-faithful
|
||||
* so LeakSanitizer would flag an orphaned/leaked evicted endpoint. */
|
||||
|
||||
extern int on_standby_connected_for_test(endpoint_t *ep);
|
||||
|
||||
/* Production teardown of an outbound standby channel (removes from
|
||||
* standby_endpoints + repl, frees ep->data); exposed for test teardown. */
|
||||
extern int standby_ep_err_handler_primary_for_test(endpoint_t *ep);
|
||||
|
||||
/* Test-observable counter: how many stale same-peer channels on_standby_connected
|
||||
* evicted while resolving ownership (raft-drive-async Task 2.2). */
|
||||
extern _Atomic int ha_standby_evicted_count_for_test;
|
||||
|
||||
/* Allocate + init a heap endpoint wired as a standby channel for peer_idx, the
|
||||
* way ha_connect_prep_peer / a promoted vote-ep would leave it (production
|
||||
* teardown via standby_ep_err_handler_primary frees ep->data + endpoint, so the
|
||||
* evicted one must be reclaimed without a leak). */
|
||||
static endpoint_t *own_make_standby_ep(ha_state_t *ha, cluster_t *c, int peer_idx)
|
||||
{
|
||||
endpoint_t *ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
endpoint_init(ep, AF_INET, SOCK_STREAM, 0, 0);
|
||||
/* No live iomgr in this unit fixture: leave ep->iomgr NULL so the eviction
|
||||
* path skips iomgr_unmonitor (it would epoll_ctl on a bogus fd). The
|
||||
* ownership-resolution logic under test (array + replicator) is unaffected. */
|
||||
ep->iomgr = NULL;
|
||||
standby_ep_ctx_build_for_test(ep, ha, c, peer_idx);
|
||||
return ep;
|
||||
}
|
||||
|
||||
static void own_setup_primary(cluster_t *c, myconf_t *conf)
|
||||
{
|
||||
memset(c, 0, sizeof(*c));
|
||||
memset(conf, 0, sizeof(*conf));
|
||||
init_ha_primary_with_standby_eps(&c->ha);
|
||||
conf->peers_cfg.candidates_count = 3;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
snprintf(conf->peers_cfg.candidates[i].name,
|
||||
sizeof(conf->peers_cfg.candidates[i].name), "ctrl%d", i);
|
||||
snprintf(conf->peers_cfg.candidates[i].hostname,
|
||||
sizeof(conf->peers_cfg.candidates[i].hostname), "127.0.0.1");
|
||||
conf->peers_cfg.candidates[i].port = 19000 + i;
|
||||
}
|
||||
c->myconf = conf;
|
||||
c->ha.my_priority = 0;
|
||||
c->myhost = &c->myhost_info;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test 18 (Req 2.6/4.4): registering a NEW outbound channel for a peer that
|
||||
* ALREADY has a live standby_endpoints entry (different ep, same peer_idx)
|
||||
* resolves to exactly ONE live channel — the new one — and tears the stale one
|
||||
* down (no dual connection, no leak).
|
||||
*
|
||||
* RED (before the resolution lands): on_standby_connected only de-dups by ep
|
||||
* pointer, so both eps remain → count == 2 (dual live connection).
|
||||
*/
|
||||
static void test_reconnect_resolves_dual_channel_same_peer(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t conf;
|
||||
own_setup_primary(&c, &conf);
|
||||
|
||||
atomic_store(&ha_standby_evicted_count_for_test, 0);
|
||||
|
||||
/* Stale prior channel for peer 2 already registered (e.g. a promoted
|
||||
* vote-ep whose RDHUP has not yet been processed). */
|
||||
endpoint_t *stale = own_make_standby_ep(&c.ha, &c, 2);
|
||||
zarray_store(c.ha.standby_endpoints, stale);
|
||||
TEST_ASSERT_EQUAL_INT(1, c.ha.standby_endpoints->count);
|
||||
|
||||
/* New reconnect to the SAME peer 2 completes and registers. */
|
||||
endpoint_t *fresh = own_make_standby_ep(&c.ha, &c, 2);
|
||||
on_standby_connected_for_test(fresh);
|
||||
|
||||
/* Exactly one live channel for peer 2 — the fresh one. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, c.ha.standby_endpoints->count,
|
||||
"Req 2.6/4.4: a reconnect for a peer with an existing channel must "
|
||||
"leave exactly ONE live outbound channel (no dual connection)");
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(fresh, zarray_get(c.ha.standby_endpoints, 0),
|
||||
"Req 2.6: the survivor must be the freshly-connected channel so no "
|
||||
"queued message is delivered down the dead duplicate");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1,
|
||||
atomic_load(&ha_standby_evicted_count_for_test),
|
||||
"the stale same-peer channel must be evicted exactly once");
|
||||
|
||||
/* fresh is owned by standby_endpoints; tear it down the production way so
|
||||
* LSan sees the heap ep + ctx reclaimed (the err handler frees both). */
|
||||
standby_ep_err_handler_primary_for_test(fresh);
|
||||
endpoint_fini(fresh);
|
||||
free(fresh);
|
||||
|
||||
fini_ha_primary_with_standby_eps(&c.ha);
|
||||
printf("test_reconnect_resolves_dual_channel_same_peer: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Test 19 (Req 2.6): registering a NEW channel for a DIFFERENT peer does NOT
|
||||
* evict an existing channel for another peer — resolution is strictly
|
||||
* per-peer, so unrelated peers' channels and their queued messages are
|
||||
* untouched.
|
||||
*/
|
||||
static void test_reconnect_preserves_other_peer_channel(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t conf;
|
||||
own_setup_primary(&c, &conf);
|
||||
|
||||
atomic_store(&ha_standby_evicted_count_for_test, 0);
|
||||
|
||||
endpoint_t *peer1 = own_make_standby_ep(&c.ha, &c, 1);
|
||||
zarray_store(c.ha.standby_endpoints, peer1);
|
||||
|
||||
endpoint_t *peer2 = own_make_standby_ep(&c.ha, &c, 2);
|
||||
on_standby_connected_for_test(peer2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(2, c.ha.standby_endpoints->count,
|
||||
"Req 2.6: a reconnect to one peer must not evict another peer's "
|
||||
"live channel");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
atomic_load(&ha_standby_evicted_count_for_test),
|
||||
"no eviction when the peer_idx differs");
|
||||
|
||||
standby_ep_err_handler_primary_for_test(peer1);
|
||||
endpoint_fini(peer1);
|
||||
free(peer1);
|
||||
standby_ep_err_handler_primary_for_test(peer2);
|
||||
endpoint_fini(peer2);
|
||||
free(peer2);
|
||||
|
||||
fini_ha_primary_with_standby_eps(&c.ha);
|
||||
printf("test_reconnect_preserves_other_peer_channel: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Suite entry point ──────────────────────────────────────────────────── */
|
||||
|
||||
void test_ha_liveness(void)
|
||||
|
|
@ -1145,5 +1301,8 @@ void test_ha_liveness(void)
|
|||
RUN_TEST(test_reconnect_offload_tick_returns_promptly);
|
||||
RUN_TEST(test_reconnect_inline_tick_blocks_RED);
|
||||
RUN_TEST(test_reconnect_offload_dedups_per_peer);
|
||||
/* raft-drive-async Task 2.2 — inbound/outbound ownership (Req 2.6, 4.4) */
|
||||
RUN_TEST(test_reconnect_resolves_dual_channel_same_peer);
|
||||
RUN_TEST(test_reconnect_preserves_other_peer_channel);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue