feat(libxce-failover): endpoint rotator — hint-first target selection + master rotation; keep api_ctx_t decls private (task 2.2)

This commit is contained in:
Rongsong Shen 2026-06-17 02:56:48 +00:00
parent fa2ac05036
commit c7e11a4e9e
5 changed files with 300 additions and 5 deletions

View file

@ -1,15 +1,26 @@
/*
* client_failover.c — StatusClassifier for the libxce-failover spec (task 2.1).
* client_failover.c — StatusClassifier (task 2.1) + EndpointRotator (task 2.2)
* for the libxce-failover spec.
*
* Pure mapping from a reply status / transport rc to a failover action. No I/O,
* no globals. See design.md "Components and Interfaces → StatusClassifier" and
* requirements 2.5, 4.1, 4.2, 4.3, 4.5, 5.2.
* StatusClassifier: pure mapping from a reply status / transport rc to a
* failover action. No I/O, no globals (requirements 2.5, 4.1-4.3, 4.5, 5.2).
*
* EndpointRotator: choose and connect the next failover target. The target
* SELECTION (xce_failover_next_target) is a pure, socket-free helper so it is
* unit-testable; xce_failover_rotate() is a thin wrapper that closes the stale
* connection and reconnects the same endpoint object (requirements 2.1, 2.2,
* 2.3, 4.4, 8.4). See design.md "Components and Interfaces → EndpointRotator".
*/
#define _GNU_SOURCE
#include "xce/client_failover.h"
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include "client_internal.h" /* api_ctx_t + EndpointRotator declarations */
#include "config.h" /* XCE_MAXHOSTNAME_LEN */
#include "netio.h" /* endpoint_init / endpoint_connect / endpoint_fini */
#include "xce/errnox.h"
/*
@ -58,3 +69,89 @@ xce_fo_action_t xce_failover_classify(int status, bool is_write, int attempt)
* status — is non-retriable and surfaced to the caller (4.5). */
return XCE_FO_TERMINAL;
}
/* ════════════════════════════════════════════════════════════════════════
* EndpointRotator — target selection (pure) + connect wrapper (thin)
* ════════════════════════════════════════════════════════════════════════ */
/* True if a redirect-address hint is currently set on the ctx. The hint is a
* NUL-terminated host; an empty host ("") means "no hint" (xcectx.h). */
static bool fo_has_hint(const api_ctx_t *ac)
{
return ac->ctx.redirect_hint_host[0] != '\0';
}
/* Clear (consume) the redirect-address hint on the ctx. One-shot + advisory. */
static void fo_clear_hint(api_ctx_t *ac)
{
ac->ctx.redirect_hint_host[0] = '\0';
ac->ctx.redirect_hint_port = 0;
}
int xce_failover_next_target(api_ctx_t *ac, char *host_out, size_t host_sz,
uint16_t *port_out)
{
if (ac == NULL || host_out == NULL || host_sz == 0 || port_out == NULL)
return -EINVAL;
/* Hint-first (2.2, 8.3): if the last reply carried a redirect address, try
* it first and CONSUME it. The cursor is intentionally left untouched so a
* failed hint falls through to normal rotation on the next call (4.4) —
* correctness never depends on the hint. */
if (fo_has_hint(ac)) {
strncpy(host_out, ac->ctx.redirect_hint_host, host_sz - 1);
host_out[host_sz - 1] = '\0';
*port_out = ac->ctx.redirect_hint_port;
fo_clear_hint(ac);
return 0;
}
/* No hint: advance the sticky master cursor to the NEXT configured master,
* wrapping around (2.1, 8.4). */
if (ac->conf == NULL || ac->conf->nmasters <= 0)
return -ENOENT;
int next = (ac->failover.cur_master + 1) % ac->conf->nmasters;
ac->failover.cur_master = next;
const char *host = ac->conf->masters[next];
if (host == NULL)
return -ENOENT;
strncpy(host_out, host, host_sz - 1);
host_out[host_sz - 1] = '\0';
*port_out = (uint16_t)ac->conf->master_ports[next];
return 0;
}
int xce_failover_rotate(api_ctx_t *ac)
{
if (ac == NULL)
return -EINVAL;
char host[XCE_MAXHOSTNAME_LEN] = { 0 };
uint16_t port = 0;
int rc = xce_failover_next_target(ac, host, sizeof(host), &port);
if (rc < 0)
return rc;
/* Close the stale connection FIRST, then reconnect the SAME endpoint
* object so reply routing to ctx->wbuf (via ctx->ep == &ac->ep) is
* preserved. endpoint_fini() closes the fd, frees the SSL session, and
* clears handlers/peer; endpoint_init() recreates the socket fd and
* re-arms the object before endpoint_connect() drives the handshake.
*
* The live socket/handshake path is exercised by the E2E task (6.2); this
* wrapper keeps the I/O surface thin around the pure selector above. */
endpoint_fini(&ac->ep);
rc = endpoint_init(&ac->ep, AF_INET, SOCK_STREAM, 0, 0);
if (rc < 0)
return rc;
endpoint_set_peer(&ac->ep, &ac->ctx.myinfo);
rc = endpoint_connect(&ac->ep, host, (int)port);
return rc < 0 ? rc : 0;
}