feat(libxce-failover): best-effort single-shot reconnect for fire-and-forget ops (notify/heartbeat/nop) (task 5.2)
This commit is contained in:
parent
4345974b54
commit
fdb8938059
5 changed files with 210 additions and 4 deletions
|
|
@ -1395,6 +1395,125 @@ static void test_failover_call_success_runs_once(void)
|
|||
printf("test_failover_call_success_runs_once: PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Task 5.2 — Best-effort reconnect for fire-and-forget ops.
|
||||
*
|
||||
* Fire-and-forget ops (notify_*, heartbeat, nop) send a *_req only — there is
|
||||
* NO reply to classify, so they do NOT use the full failover loop (no status
|
||||
* classification, no no-primary backoff, no retry budget). Putting a heartbeat
|
||||
* through the retry loop would let it block on the no-primary backoff budget,
|
||||
* which is explicitly forbidden. Instead the FF path makes AT MOST ONE
|
||||
* best-effort transport reconnect when the connection is KNOWN-DEAD, then does
|
||||
* a single-shot send (which may be lost — acceptable, redelivery is deferred to
|
||||
* worker-rejoin-resync).
|
||||
*
|
||||
* The "known-dead" signal is ctx->state == XCE_CTX_ERR (set by the disconnect
|
||||
* path after a detected transport failure). xcecli_ff_needs_reconnect is the
|
||||
* pure predicate factored out so this decision is unit-testable socket-free:
|
||||
* ERR → true (reconnect once, best-effort)
|
||||
* RUN → false (connection live, send directly)
|
||||
* DOWN → false (deliberately stopped — do not resurrect)
|
||||
* INIT → false (not yet started)
|
||||
*
|
||||
* Requirements: 1.1 (all ops go through failover machinery — FF via the
|
||||
* best-effort transport-reconnect variant), 5.3 (no silent drop on a path that
|
||||
* CAN reconnect; a known-dead connection is repaired before the single shot).
|
||||
* Full live reconnect-then-send is E2E task 6.2.
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Req 1.1/5.3: a known-dead (ERR) connection needs a best-effort reconnect. */
|
||||
static void test_ff_needs_reconnect_on_err(void)
|
||||
{
|
||||
api_ctx_t ac;
|
||||
memset(&ac, 0, sizeof(ac));
|
||||
ac.ctx.state = XCE_CTX_ERR;
|
||||
|
||||
TEST_ASSERT_TRUE(xcecli_ff_needs_reconnect(&ac));
|
||||
|
||||
printf("test_ff_needs_reconnect_on_err: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 1.1: a live (RUN) connection needs NO reconnect — send directly. */
|
||||
static void test_ff_no_reconnect_on_run(void)
|
||||
{
|
||||
api_ctx_t ac;
|
||||
memset(&ac, 0, sizeof(ac));
|
||||
ac.ctx.state = XCE_CTX_RUN;
|
||||
|
||||
TEST_ASSERT_FALSE(xcecli_ff_needs_reconnect(&ac));
|
||||
|
||||
printf("test_ff_no_reconnect_on_run: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 1.1: a DOWN (deliberately stopped) or INIT (not yet started) ctx is NOT
|
||||
* resurrected by a fire-and-forget op — only a transport-ERR connection is. */
|
||||
static void test_ff_no_reconnect_on_down_or_init(void)
|
||||
{
|
||||
api_ctx_t ac;
|
||||
|
||||
memset(&ac, 0, sizeof(ac));
|
||||
ac.ctx.state = XCE_CTX_DOWN;
|
||||
TEST_ASSERT_FALSE(xcecli_ff_needs_reconnect(&ac));
|
||||
|
||||
memset(&ac, 0, sizeof(ac));
|
||||
ac.ctx.state = XCE_CTX_INIT;
|
||||
TEST_ASSERT_FALSE(xcecli_ff_needs_reconnect(&ac));
|
||||
|
||||
printf("test_ff_no_reconnect_on_down_or_init: PASS\n");
|
||||
}
|
||||
|
||||
/* Guard: a NULL api_ctx is rejected as "no reconnect" without crashing. */
|
||||
static void test_ff_needs_reconnect_null_safe(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(xcecli_ff_needs_reconnect(NULL));
|
||||
printf("test_ff_needs_reconnect_null_safe: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 5.3 (structural): the fire-and-forget path is single-shot — it makes AT
|
||||
* MOST ONE reconnect attempt and NEVER consumes the no-primary retry budget /
|
||||
* sleeps. We prove this structurally: the FF macro drives the body exactly once
|
||||
* regardless of the rc it returns (a "lost" send rc is surfaced verbatim, NOT
|
||||
* retried), and on a known-dead ctx the single reconnect attempt fires exactly
|
||||
* once before that single body invocation. */
|
||||
typedef struct {
|
||||
api_ctx_t *ac;
|
||||
int body_calls; /* number of times the FF body ran */
|
||||
int body_rc; /* rc the body returns each call */
|
||||
} fo_ff_probe_t;
|
||||
|
||||
static int fo_ff_body(fo_ff_probe_t *p)
|
||||
{
|
||||
p->body_calls++;
|
||||
return p->body_rc;
|
||||
}
|
||||
|
||||
static void test_ff_call_is_single_shot_no_budget(void)
|
||||
{
|
||||
xce_client_config_t conf;
|
||||
api_ctx_t ac;
|
||||
fo_make_client_masters(&conf);
|
||||
fo_make_api_ctx(&ac, &conf);
|
||||
TEST_ASSERT_EQUAL_INT(0, mylock_init(&ac.failover.lock));
|
||||
ac.failover.retry = XCE_RETRY_DEFAULTS;
|
||||
/* a live RUN ctx: no reconnect, just the single-shot send */
|
||||
ac.ctx.state = XCE_CTX_RUN;
|
||||
|
||||
fo_ff_probe_t p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
p.ac = ∾
|
||||
|
||||
/* a "lost" fire-and-forget send (e.g. -ECONNRESET) is surfaced verbatim and
|
||||
* is NOT retried — the body must run exactly once. */
|
||||
p.body_rc = -ECONNRESET;
|
||||
int rc = XCECLI_FF_CALL(&ac, fo_ff_body(&p));
|
||||
mylock_fini(&ac.failover.lock);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-ECONNRESET, rc); /* best-effort: surfaced as-is */
|
||||
TEST_ASSERT_EQUAL_INT(1, p.body_calls); /* single shot, no retry loop */
|
||||
|
||||
printf("test_ff_call_is_single_shot_no_budget: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Suite entry point ──────────────────────────────────────────────── */
|
||||
|
||||
void test_libxce_retry(void)
|
||||
|
|
@ -1440,5 +1559,10 @@ void test_libxce_retry(void)
|
|||
RUN_TEST(test_invoke_rejoin_null_ctx_safe);
|
||||
RUN_TEST(test_failover_call_serialises_shared_ctx);
|
||||
RUN_TEST(test_failover_call_success_runs_once);
|
||||
RUN_TEST(test_ff_needs_reconnect_on_err);
|
||||
RUN_TEST(test_ff_no_reconnect_on_run);
|
||||
RUN_TEST(test_ff_no_reconnect_on_down_or_init);
|
||||
RUN_TEST(test_ff_needs_reconnect_null_safe);
|
||||
RUN_TEST(test_ff_call_is_single_shot_no_budget);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue