feat(libxce-failover): best-effort single-shot reconnect for fire-and-forget ops (notify/heartbeat/nop) (task 5.2)

This commit is contained in:
Rongsong Shen 2026-06-17 04:08:58 +00:00
parent 4345974b54
commit fdb8938059
5 changed files with 210 additions and 4 deletions

View file

@ -192,6 +192,60 @@ bool xcecli_retry_next(api_ctx_t* ac, int* rc, bool is_write,
*/
int xcecli_ctx_recover(api_ctx_t* ac);
/*
* xcecli_ff_needs_reconnect (task 5.2) — PURE predicate for the fire-and-forget
* best-effort reconnect decision. Fire-and-forget ops (notify_*, heartbeat, nop)
* send a *_req only — there is NO reply to classify, so they must NOT go through
* the full failover retry loop (no status classification, no no-primary backoff,
* no retry budget; a heartbeat must never block on the backoff budget). 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). Returns true ONLY for ERR so that a
* deliberately-stopped (DOWN) or not-yet-started (INIT) ctx is never resurrected
* by a fire-and-forget op. NULL-safe: returns false. Socket-free + side-effect
* free so it is unit-testable.
*/
bool xcecli_ff_needs_reconnect(const api_ctx_t* ac);
/*
* XCECLI_FF_CALL(ac, EXPR) — the fire-and-forget best-effort wrapper (task 5.2).
* Mirrors XCECLI_FAILOVER_CALL's per-ctx locking (CI2 — shared-ctx safety) but
* is single-shot, NOT a retry loop: there is no reply to classify for an FF op,
* so the full failover machinery (classification, no-primary backoff, retry
* budget) is deliberately NOT used. Putting a heartbeat through the retry loop
* would let it block on the no-primary backoff budget — explicitly forbidden.
*
* Behaviour, holding ac->failover.lock for the whole sequence:
* 1. if the connection is KNOWN-DEAD (xcecli_ff_needs_reconnect), attempt ONE
* best-effort reconnect: xce_failover_rotate(ac) + xcecli_ctx_recover(ac).
* A failed reconnect is non-fatal — the send below is simply best-effort.
* 2. evaluate EXPR exactly ONCE (the unchanged one-shot xce_* fire-and-forget
* send) and yield its rc verbatim. A lost send rc is surfaced as-is, never
* retried.
*
* ac — api_ctx_t* (already NULL-checked by the caller's guard)
* EXPR — the unchanged one-shot xce_* *_req-only call
*
* Implemented as a GCC/clang statement-expression so it yields the send rc and
* keeps each public xcecli_* signature/return convention intact (R1.3).
*/
#define XCECLI_FF_CALL(ac, EXPR) \
__extension__({ \
api_ctx_t* _ff_ac = (ac); \
int _ff_rc; \
mylock_lock(&_ff_ac->failover.lock); \
if (xcecli_ff_needs_reconnect(_ff_ac)) { \
(void)xce_failover_rotate(_ff_ac); \
xcecli_ctx_recover(_ff_ac); \
} \
_ff_rc = (EXPR); \
mylock_unlock(&_ff_ac->failover.lock); \
_ff_rc; \
})
/*
* xcecli_invoke_rejoin (task 4.1) — invoke the consumer rejoin hook once after a
* successful reconnect/recovery and BEFORE the interrupted op resumes. Socket-