myxce/src/lib/cluster.c
Rongsong Shen 0c11989da7 feat: P2.5-1/2/2a/3/8 xce_cluster_config_t, xce_connect_cluster, xce_reconnect_step
Add libxce cluster discovery and auto-reconnect state machine:

- src/include/xce/cluster.h: xce_candidate_t, xce_cluster_config_t,
  xce_reconnect_ctx_t types; full API with test-seam hook
  xce_cluster_set_connect_fn()
- src/lib/cluster.c: xce_cluster_config_validate() (rejects invalid
  candidate counts, empty hostname/port, missing identity/ca_cert),
  xce_connect_cluster() (candidate fallback with single sweep),
  xce_reconnect_step() state machine (IDLE→CONNECTING→CONNECTED,
  with exponential backoff; nanosleep skipped when mock is active)
- src/lib/meson.build: add cluster.c to libxce_src and
  test_cluster.c to libxce_test_src (P2.5-2a)
- src/lib/testsuite/test_cluster.c: 9 TDD tests covering validation,
  first-candidate success, fallback, all-fail NULL return, reconnect
  to CONNECTED, and backoff state transition (P2.5-8)

All 9 cluster tests pass; 3 pre-existing unrelated failures unchanged.
2026-05-08 02:16:08 +00:00

151 lines
4.8 KiB
C

/* src/lib/cluster.c — cluster discovery and reconnect (P2.5-2/3)
*
* xce_connect_cluster() iterates the candidate list and returns the first
* successful endpoint_t* (cast to void*). In production the real SSL
* connect is performed via do_connect(); unit tests override s_connect_fn.
*
* xce_reconnect_step() drives a non-blocking state machine:
* IDLE → CONNECTING → CONNECTED (happy path)
* IDLE → CONNECTING → BACKOFF → CONNECTING → … (retrying)
*
* Backoff sleep is skipped when s_connect_fn is set (i.e., in unit tests)
* so tests do not need to wait for real timeouts.
*/
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "xce/cluster.h"
/* ── Test seam ─────────────────────────────────────────────────── */
static xce_connect_fn_t s_connect_fn = NULL;
void xce_cluster_set_connect_fn(xce_connect_fn_t fn)
{
s_connect_fn = fn;
}
/* do_connect: calls the mock when set, otherwise returns NULL (stub for
* the real SSL connect via netio — to be wired in a later task). */
static void* do_connect(const char* host, int port, const char* ca_cert)
{
if (s_connect_fn)
return s_connect_fn(host, port, ca_cert);
/* Production path: real SSL connect via netio — stub for now */
(void)host;
(void)port;
(void)ca_cert;
return NULL;
}
/* ── Validation ────────────────────────────────────────────────── */
int xce_cluster_config_validate(const xce_cluster_config_t* cfg)
{
if (!cfg)
return -EINVAL;
if (cfg->candidates_count < 1 || cfg->candidates_count > MAX_HA_CANDIDATES)
return -EINVAL;
for (int i = 0; i < cfg->candidates_count; i++) {
if (cfg->candidates[i].hostname[0] == '\0')
return -EINVAL;
if (cfg->candidates[i].port <= 0)
return -EINVAL;
}
if (cfg->ca_cert_path[0] == '\0')
return -EINVAL;
if (cfg->identity[0] == '\0')
return -EINVAL;
if (cfg->candidates_count > 3)
fprintf(stderr,
"WARN: HA cluster has %d candidates; validated for <=3 only\n",
cfg->candidates_count);
return 0;
}
/* ── Connect with candidate fallback ───────────────────────────── */
void* xce_connect_cluster(xce_cluster_config_t* cfg)
{
if (xce_cluster_config_validate(cfg) != 0)
return NULL;
for (int i = 0; i < cfg->candidates_count; i++) {
void* ep = do_connect(cfg->candidates[i].hostname,
cfg->candidates[i].port,
cfg->ca_cert_path);
if (ep)
return ep;
}
return NULL;
}
/* ── Reconnect state machine ───────────────────────────────────── */
int xce_reconnect_init(xce_reconnect_ctx_t* rctx, xce_cluster_config_t* cfg)
{
if (!rctx || !cfg)
return -EINVAL;
memset(rctx, 0, sizeof(*rctx));
rctx->config = cfg;
rctx->state = XCE_RECONNECT_IDLE;
rctx->backoff_ms = XCE_RECONNECT_INITIAL_BACKOFF_MS;
return 0;
}
int xce_reconnect_step(xce_reconnect_ctx_t* rctx)
{
if (!rctx)
return -EINVAL;
switch (rctx->state) {
case XCE_RECONNECT_IDLE:
rctx->state = XCE_RECONNECT_CONNECTING;
return 0; /* next step does the actual connect */
case XCE_RECONNECT_CONNECTING: {
void* ep = xce_connect_cluster(rctx->config);
if (ep) {
rctx->endpoint = ep;
rctx->state = XCE_RECONNECT_CONNECTED;
rctx->attempt = 0;
rctx->backoff_ms = XCE_RECONNECT_INITIAL_BACKOFF_MS;
} else {
rctx->attempt++;
rctx->state = XCE_RECONNECT_BACKOFF;
}
return 0;
}
case XCE_RECONNECT_BACKOFF: {
/* Skip sleep in test context (mock connect set) to keep tests fast */
if (!s_connect_fn) {
struct timespec ts = {
.tv_sec = rctx->backoff_ms / 1000,
.tv_nsec = (long)(rctx->backoff_ms % 1000) * 1000000L,
};
nanosleep(&ts, NULL);
}
/* Exponential backoff with cap */
rctx->backoff_ms *= XCE_RECONNECT_BACKOFF_MULTIPLIER;
if (rctx->backoff_ms > XCE_RECONNECT_MAX_BACKOFF_MS)
rctx->backoff_ms = XCE_RECONNECT_MAX_BACKOFF_MS;
rctx->state = XCE_RECONNECT_CONNECTING;
return 0;
}
case XCE_RECONNECT_CONNECTED:
case XCE_RECONNECT_FAILED:
return -1;
}
return 0;
}
void xce_reconnect_fini(xce_reconnect_ctx_t* rctx)
{
if (rctx)
memset(rctx, 0, sizeof(*rctx));
}