feat(libxce-failover): reconnect recovery — ctx ERR->RUN reset + stale wbuf drain (fixes post-failover EINVAL) (task 3.2)

This commit is contained in:
Rongsong Shen 2026-06-17 03:36:24 +00:00
parent 84c5e32c2c
commit 12284bc3f5
5 changed files with 170 additions and 1 deletions

View file

@ -1021,6 +1021,112 @@ static void test_decide_transport_exhaustion_keeps_last_rc(void)
printf("test_decide_transport_exhaustion_keeps_last_rc: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 3.2 — Reconnect recovery (xcecli_ctx_recover) RED→GREEN tests.
*
* After a transport failure the disconnect path leaves ctx->state == XCE_CTX_ERR
* (xcectx.c:174) and may have injected a stale transport status into the single-
* slot ctx->wbuf (xcectx.c:180). xcecli_ctx_recover repairs the ctx after a
* successful reconnect, with NO socket I/O so it is unit-testable:
* - CI2 state reset: ERR → RUN (only ERR; RUN/DOWN/INIT left as-is) so the
* next xce_* op passes the xceapi XCE_CTX_RUN guard instead of -EINVAL.
* - CI1 stale-reply drain: any entry already sitting in wbuf is drained with a
* non-blocking get, so the next attempt's cbuf_get cannot consume a prior-
* attempt reply.
*
* Requirements: 1.2 (complete next request against new primary, no caller
* change), 4.4 (transport → reconnect/rotate and resume).
* ════════════════════════════════════════════════════════════════════════ */
/* CI2: an ERR-state ctx is restored to RUN (the reported -EINVAL bug fix). */
static void test_recover_resets_err_state_to_run(void)
{
api_ctx_t ac;
memset(&ac, 0, sizeof(ac));
ac.ctx.state = XCE_CTX_ERR;
/* wbuf left un-init: recover must tolerate a recover that has nothing to
* drain (init it so the drain path is also exercised safely). */
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&ac.ctx.wbuf, 1, sizeof(int)));
int rc = xcecli_ctx_recover(&ac);
cbuf_fini(&ac.ctx.wbuf);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(XCE_CTX_RUN, ac.ctx.state);
printf("test_recover_resets_err_state_to_run: PASS\n");
}
/* CI2: a ctx already in RUN is left in RUN; only ERR→RUN is performed. */
static void test_recover_leaves_run_state_untouched(void)
{
api_ctx_t ac;
memset(&ac, 0, sizeof(ac));
ac.ctx.state = XCE_CTX_RUN;
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&ac.ctx.wbuf, 1, sizeof(int)));
int rc = xcecli_ctx_recover(&ac);
cbuf_fini(&ac.ctx.wbuf);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(XCE_CTX_RUN, ac.ctx.state);
printf("test_recover_leaves_run_state_untouched: PASS\n");
}
/* CI2: a DOWN ctx is NOT flipped to RUN — only an ERR transport state is
* repaired; a deliberately-stopped ctx stays down. */
static void test_recover_leaves_down_state_untouched(void)
{
api_ctx_t ac;
memset(&ac, 0, sizeof(ac));
ac.ctx.state = XCE_CTX_DOWN;
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&ac.ctx.wbuf, 1, sizeof(int)));
int rc = xcecli_ctx_recover(&ac);
cbuf_fini(&ac.ctx.wbuf);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(XCE_CTX_DOWN, ac.ctx.state);
printf("test_recover_leaves_down_state_untouched: PASS\n");
}
/* CI1: a stale status already sitting in wbuf is drained, so a subsequent
* non-blocking get finds the channel EMPTY (the prior-attempt reply was
* discarded, not handed to the next attempt's cbuf_get). */
static void test_recover_drains_stale_wbuf_entry(void)
{
api_ctx_t ac;
memset(&ac, 0, sizeof(ac));
ac.ctx.state = XCE_CTX_ERR;
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&ac.ctx.wbuf, 1, sizeof(int)));
/* mirror handle_disconnect: a stale transport status sits in the channel */
int stale = -ECONNRESET;
TEST_ASSERT_EQUAL_INT(0, cbuf_tryput(&ac.ctx.wbuf, &stale));
int rc = xcecli_ctx_recover(&ac);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(XCE_CTX_RUN, ac.ctx.state);
/* the stale entry was drained: a non-blocking get now finds nothing */
int drained = 0;
int g = cbuf_tryget(&ac.ctx.wbuf, &drained);
cbuf_fini(&ac.ctx.wbuf);
TEST_ASSERT_EQUAL_INT(-EAGAIN, g); /* channel empty after recover */
printf("test_recover_drains_stale_wbuf_entry: PASS\n");
}
/* Guard: a NULL api_ctx is rejected without crashing. */
static void test_recover_null_ctx_safe(void)
{
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctx_recover(NULL));
printf("test_recover_null_ctx_safe: PASS\n");
}
/* ── Suite entry point ──────────────────────────────────────────────── */
void test_libxce_retry(void)
@ -1054,5 +1160,10 @@ void test_libxce_retry(void)
RUN_TEST(test_decide_terminal_surfaces_unchanged);
RUN_TEST(test_decide_dup_on_resent_write_is_success);
RUN_TEST(test_decide_transport_exhaustion_keeps_last_rc);
RUN_TEST(test_recover_resets_err_state_to_run);
RUN_TEST(test_recover_leaves_run_state_untouched);
RUN_TEST(test_recover_leaves_down_state_untouched);
RUN_TEST(test_recover_drains_stale_wbuf_entry);
RUN_TEST(test_recover_null_ctx_safe);
UNITY_END();
}