feat(ha-config-bootstrap): implement xce_retry_config_t and xce_request_with_retry (tasks 10.1, 10.2)

This commit is contained in:
Rongsong Shen 2026-05-29 03:00:34 +00:00
parent bb8e531a12
commit 0ec0ffa7e3
4 changed files with 163 additions and 12 deletions

View file

@ -5,6 +5,7 @@
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "xce/errnox.h" /* E_XCE_NOT_READY, E_XCE_STANDBY */
#include <openssl/ssl.h>
@ -1497,3 +1498,110 @@ int xce_clear_reason_req(endpoint_t* ep, xce_clear_reason_t* reason)
return xce_request(ep, &request);
}
/* ── Task 10.2: Client-Side Transparent Retry (Req 7.1–7.6) ────────────────
*
* xce_retry_send_hook: test-injectable send+receive round-trip.
* NULL in production; set by unit tests to return controlled replies.
*/
int (*xce_retry_send_hook)(endpoint_t *ep, xce_request_t *req,
xce_reply_t *reply_out) = NULL;
const xce_retry_config_t XCE_RETRY_DEFAULTS = {
.max_retries = 20,
.initial_retry_ms = 50,
.max_retry_ms = 5000,
.backoff_multiplier = 2.0,
};
/* is_non_retriable: errors that should surface immediately without retry */
static int is_non_retriable(int status)
{
return (status == -EPERM || status == -EACCES ||
status == -EINVAL || status == -ENOENT);
}
/* ms_sleep_jittered: sleep delay_ms + random jitter via rand_r (Req 7.6) */
static void ms_sleep_jittered(int delay_ms, unsigned int *seed)
{
if (delay_ms <= 0) return;
int jitter_ms = (int)((rand_r(seed) % (delay_ms / 4 + 1)));
long total_ns = ((long)delay_ms + jitter_ms) * 1000000L;
struct timespec ts = { .tv_sec = total_ns / 1000000000L,
.tv_nsec = total_ns % 1000000000L };
nanosleep(&ts, NULL);
}
int xce_request_with_retry(endpoint_t *ep,
xce_request_t *req,
const xce_retry_config_t *cfg)
{
if (cfg == NULL)
cfg = &XCE_RETRY_DEFAULTS;
unsigned int rand_seed = (unsigned int)(uintptr_t)ep ^ (unsigned int)time(NULL);
int delay_ms = cfg->initial_retry_ms;
int last_rc = -ETIMEDOUT;
for (int attempt = 0; attempt <= cfg->max_retries; attempt++) {
xce_reply_t reply;
memset(&reply, 0, sizeof(reply));
int rc;
if (xce_retry_send_hook) {
rc = xce_retry_send_hook(ep, req, &reply);
} else {
/* Production path: fire-and-forget send; reply handling is caller's
* responsibility via the event loop. Return send error directly. */
rc = xce_request(ep, req);
if (rc != 0)
return rc;
return 0; /* async: caller reads reply via event loop */
}
/* Transport-level failure (ECONNREFUSED, ETIMEDOUT) → retry (Req 7.3) */
if (rc == -ECONNREFUSED || rc == -ETIMEDOUT) {
last_rc = rc;
if (attempt < cfg->max_retries)
ms_sleep_jittered(delay_ms, &rand_seed);
goto next_backoff;
}
/* Any other transport error → surface immediately */
if (rc != 0)
return rc;
/* Application-layer status from reply */
int status = (int)reply.status;
if (status == 0)
return 0; /* success */
last_rc = status;
/* Non-retriable errors: surface immediately (Req 7.4) */
if (is_non_retriable(status))
return status;
/* XCE_ERR_STANDBY with redirect → reconnect (Req 7.2).
* In hook mode, the hook already updated ep/redirect state.
* Just retry; redirect handling is at the transport layer. */
if (status == -E_XCE_STANDBY && reply.redirect_host != NULL
&& reply.redirect_port != 0) {
/* no extra sleep for redirect — just retry immediately */
continue;
}
/* XCE_ERR_NOT_READY or XCE_ERR_STANDBY without redirect → backoff */
if (attempt < cfg->max_retries)
ms_sleep_jittered(delay_ms, &rand_seed);
next_backoff:
{
int next = (int)((double)delay_ms * cfg->backoff_multiplier);
delay_ms = next > cfg->max_retry_ms ? cfg->max_retry_ms : next;
}
}
return last_rc;
}