feat(libxce-failover): retry decision loop — pure decide (budget/backoff/jitter, D6 exhaustion) + thin next wrapper (task 3.1)

This commit is contained in:
Rongsong Shen 2026-06-17 03:27:38 +00:00
parent cec2e647dc
commit 84c5e32c2c
4 changed files with 481 additions and 2 deletions

View file

@ -2,6 +2,7 @@
#define _CLIENT_INTERNAL_H_ 1
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
@ -9,6 +10,7 @@
#include "mylock.h"
#include "netio.h"
#include "request.h"
#include "xce/client_failover.h" /* xce_fo_action_t */
#include "xcectx.h"
/*
@ -80,4 +82,61 @@ int xce_failover_next_target(api_ctx_t* ac, char* host_out, size_t host_sz,
uint16_t* port_out);
int xce_failover_rotate(api_ctx_t* ac);
/*
* XcecliRetryLoop / decision helper (task 3.1) — see design.md
* "Components and Interfaces → xcecli_retry_next".
*
* xcecli_retry_t: per-op retry carrier. One instance lives on the caller's
* stack for the duration of a single xcecli_* op's do/while loop.
* attempt — number of re-sends so far (0 on the first send).
* delay_ms — backoff to apply BEFORE the next attempt (0 for RETRY_NOW).
* rand_seed — per-op rand_r() state for jittered backoff (6.3).
*/
typedef struct {
int attempt;
int delay_ms;
unsigned int rand_seed;
} xcecli_retry_t;
/*
* XCECLI_RETRY_INIT(ac): initialise a retry carrier from an api_ctx_t. The seed
* mixes the ctx pointer so concurrent ops on distinct ctxs jitter independently.
*/
#define XCECLI_RETRY_INIT(ac) \
((xcecli_retry_t){ .attempt = 0, \
.delay_ms = 0, \
.rand_seed = (unsigned int)(uintptr_t)(ac) })
/*
* xcecli_retry_decide(): the PURE decision core (no I/O, no sleep, no socket,
* no logging) so it is unit-testable. Given the rc returned by the last xce_*
* attempt, it:
* - classifies *rc via xce_failover_classify(*rc, is_write, st->attempt);
* - on SUCCESS applies the DUP-replay rule (sets *rc = 0) and returns false;
* - on TERMINAL returns false leaving *rc as-is;
* - on a retriable action advances st->attempt, computes st->delay_ms
* (0 for RETRY_NOW; a jittered exponential backoff bounded by
* cfg->max_retry_ms for RETRY_BACKOFF), reports the action in *action_out,
* and returns true — UNLESS the retry budget (cfg->max_retries) is now
* exhausted, in which case it returns false and, for a no-primary cause,
* sets *rc = -E_XCE_NOT_READY (a transport-only cause keeps the last rc).
*
* It does NOT reset ctx state, drain wbuf, sleep, rotate, or invoke the rejoin
* hook — those are the side-effecting wrapper's job (xcecli_retry_next) and
* later tasks (3.2 state-reset/drain, 4.1 rejoin hook).
*/
bool xcecli_retry_decide(api_ctx_t* ac, int* rc, bool is_write,
const xce_retry_config_t* cfg, xcecli_retry_t* st,
xce_fo_action_t* action_out);
/*
* xcecli_retry_next(): the side-effecting wrapper the xcecli_* do/while loops
* call. = xcecli_retry_decide() + (RETRY_BACKOFF: a real st->delay_ms sleep) +
* (any retry: xce_failover_rotate(ac) + INFO log of the rotation target, 7.1) +
* (budget exhaustion: WARN log of the attempt count, 7.2). Returns true → the
* caller re-invokes its xce_* op; false → stop, *rc is final.
*/
bool xcecli_retry_next(api_ctx_t* ac, int* rc, bool is_write,
xcecli_retry_t* st);
#endif