forked from lei.chen/myxce
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.
181 lines
6.7 KiB
C
181 lines
6.7 KiB
C
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unity/unity.h>
|
|
|
|
#include "xce/cluster.h"
|
|
|
|
/* ── Test seam: mock connect function ───────────────────────────── */
|
|
static int s_connect_call_count = 0;
|
|
static int s_connect_fail_n = 0; /* fail first N attempts, then succeed */
|
|
static char s_last_host[256] = "";
|
|
static int s_last_port = 0;
|
|
|
|
static void* mock_connect(const char* host, int port, const char* ca_cert)
|
|
{
|
|
(void)ca_cert;
|
|
strncpy(s_last_host, host, sizeof(s_last_host) - 1);
|
|
s_last_port = port;
|
|
s_connect_call_count++;
|
|
if (s_connect_call_count <= s_connect_fail_n)
|
|
return NULL;
|
|
return (void*)0xDEADBEEF; /* non-NULL = success */
|
|
}
|
|
|
|
static xce_cluster_config_t make_valid_config(void)
|
|
{
|
|
xce_cluster_config_t cfg = {0};
|
|
strncpy(cfg.candidates[0].hostname, "10.0.1.1",
|
|
XCE_MAXHOSTNAME_LEN - 1);
|
|
cfg.candidates[0].port = 7000;
|
|
strncpy(cfg.candidates[1].hostname, "10.0.1.2",
|
|
XCE_MAXHOSTNAME_LEN - 1);
|
|
cfg.candidates[1].port = 7001;
|
|
cfg.candidates_count = 2;
|
|
strncpy(cfg.ca_cert_path, "/etc/xce/ca.pem",
|
|
sizeof(cfg.ca_cert_path) - 1);
|
|
strncpy(cfg.identity, "worker-01", sizeof(cfg.identity) - 1);
|
|
return cfg;
|
|
}
|
|
|
|
/* ── Test cases ─────────────────────────────────────────────────── */
|
|
|
|
/* Test 1: validate rejects empty config */
|
|
static void test_validate_rejects_zero_candidates(void)
|
|
{
|
|
xce_cluster_config_t cfg = {0};
|
|
cfg.candidates_count = 0;
|
|
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
|
printf("test_validate_rejects_zero_candidates: PASS\n");
|
|
}
|
|
|
|
/* Test 2: validate rejects too many candidates */
|
|
static void test_validate_rejects_over_max_candidates(void)
|
|
{
|
|
xce_cluster_config_t cfg = {0};
|
|
cfg.candidates_count = MAX_HA_CANDIDATES + 1;
|
|
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
|
printf("test_validate_rejects_over_max_candidates: PASS\n");
|
|
}
|
|
|
|
/* Test 3: validate rejects missing identity */
|
|
static void test_validate_rejects_empty_identity(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
cfg.identity[0] = '\0';
|
|
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
|
printf("test_validate_rejects_empty_identity: PASS\n");
|
|
}
|
|
|
|
/* Test 4: validate accepts valid config */
|
|
static void test_validate_accepts_valid_config(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
TEST_ASSERT_EQUAL_INT(0, xce_cluster_config_validate(&cfg));
|
|
printf("test_validate_accepts_valid_config: PASS\n");
|
|
}
|
|
|
|
/* Test 5: connect succeeds on first candidate */
|
|
static void test_connect_succeeds_first_candidate(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
s_connect_call_count = 0;
|
|
s_connect_fail_n = 0;
|
|
xce_cluster_set_connect_fn(mock_connect);
|
|
void* ep = xce_connect_cluster(&cfg);
|
|
TEST_ASSERT_NOT_NULL(ep);
|
|
TEST_ASSERT_EQUAL_INT(1, s_connect_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("10.0.1.1", s_last_host);
|
|
TEST_ASSERT_EQUAL_INT(7000, s_last_port);
|
|
xce_cluster_set_connect_fn(NULL);
|
|
printf("test_connect_succeeds_first_candidate: PASS\n");
|
|
}
|
|
|
|
/* Test 6: connect falls back to second candidate when first fails */
|
|
static void test_connect_fallback_to_second_candidate(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
s_connect_call_count = 0;
|
|
s_connect_fail_n = 1; /* first attempt fails */
|
|
xce_cluster_set_connect_fn(mock_connect);
|
|
void* ep = xce_connect_cluster(&cfg);
|
|
TEST_ASSERT_NOT_NULL(ep);
|
|
TEST_ASSERT_EQUAL_INT(2, s_connect_call_count);
|
|
TEST_ASSERT_EQUAL_STRING("10.0.1.2", s_last_host);
|
|
xce_cluster_set_connect_fn(NULL);
|
|
printf("test_connect_fallback_to_second_candidate: PASS\n");
|
|
}
|
|
|
|
/* Test 7: connect returns NULL when all candidates fail */
|
|
static void test_connect_returns_null_all_fail(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
s_connect_call_count = 0;
|
|
s_connect_fail_n = 999; /* all fail */
|
|
xce_cluster_set_connect_fn(mock_connect);
|
|
void* ep = xce_connect_cluster(&cfg);
|
|
TEST_ASSERT_NULL(ep);
|
|
xce_cluster_set_connect_fn(NULL);
|
|
printf("test_connect_returns_null_all_fail: PASS\n");
|
|
}
|
|
|
|
/* Test 8: reconnect state machine — IDLE → CONNECTING → CONNECTED */
|
|
static void test_reconnect_step_reaches_connected(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
xce_reconnect_ctx_t rctx;
|
|
s_connect_call_count = 0;
|
|
s_connect_fail_n = 0;
|
|
xce_cluster_set_connect_fn(mock_connect);
|
|
xce_reconnect_init(&rctx, &cfg);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_IDLE, (int)rctx.state);
|
|
|
|
int steps = 0;
|
|
while (rctx.state != XCE_RECONNECT_CONNECTED &&
|
|
rctx.state != XCE_RECONNECT_FAILED && steps < 100) {
|
|
xce_reconnect_step(&rctx);
|
|
steps++;
|
|
}
|
|
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_CONNECTED, (int)rctx.state);
|
|
TEST_ASSERT_NOT_NULL(rctx.endpoint);
|
|
|
|
xce_reconnect_fini(&rctx);
|
|
xce_cluster_set_connect_fn(NULL);
|
|
printf("test_reconnect_step_reaches_connected: PASS\n");
|
|
}
|
|
|
|
/* Test 9: reconnect state machine backs off on failure */
|
|
static void test_reconnect_step_backs_off_on_failure(void)
|
|
{
|
|
xce_cluster_config_t cfg = make_valid_config();
|
|
cfg.candidates_count = 1; /* only one candidate so it hits backoff */
|
|
xce_reconnect_ctx_t rctx;
|
|
s_connect_call_count = 0;
|
|
s_connect_fail_n = 999; /* always fail */
|
|
xce_cluster_set_connect_fn(mock_connect);
|
|
xce_reconnect_init(&rctx, &cfg);
|
|
|
|
xce_reconnect_step(&rctx); /* IDLE → CONNECTING (attempt connect) */
|
|
xce_reconnect_step(&rctx); /* CONNECTING → BACKOFF (all candidates exhausted) */
|
|
/* When mock is set, sleep is skipped — state must be BACKOFF */
|
|
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_BACKOFF, (int)rctx.state);
|
|
TEST_ASSERT_TRUE(rctx.backoff_ms > 0);
|
|
|
|
xce_reconnect_fini(&rctx);
|
|
xce_cluster_set_connect_fn(NULL);
|
|
printf("test_reconnect_step_backs_off_on_failure: PASS\n");
|
|
}
|
|
|
|
/* ── Test group entry point ─────────────────────────────────────── */
|
|
void test_cluster(void)
|
|
{
|
|
RUN_TEST(test_validate_rejects_zero_candidates);
|
|
RUN_TEST(test_validate_rejects_over_max_candidates);
|
|
RUN_TEST(test_validate_rejects_empty_identity);
|
|
RUN_TEST(test_validate_accepts_valid_config);
|
|
RUN_TEST(test_connect_succeeds_first_candidate);
|
|
RUN_TEST(test_connect_fallback_to_second_candidate);
|
|
RUN_TEST(test_connect_returns_null_all_fail);
|
|
RUN_TEST(test_reconnect_step_reaches_connected);
|
|
RUN_TEST(test_reconnect_step_backs_off_on_failure);
|
|
}
|