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

@ -26,8 +26,11 @@
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unity/unity.h>
#include "client_internal.h" /* api_ctx_t, xce_failover_state_t */
#include "failover_harness.h"
#include "request.h"
#include "xce/client_failover.h"
@ -450,6 +453,170 @@ static void test_classify_unknown_is_terminal(void)
printf("test_classify_unknown_is_terminal: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 2.2 — EndpointRotator target selection (xce_failover_next_target)
* RED→GREEN tests. Pure, socket-free target-selection logic; the
* live connect path is covered by E2E task 6.2.
* ════════════════════════════════════════════════════════════════════════ */
/* Build a 3-master client config (distinct host:port) the rotator selects over.
* Strings are static-duration so the api_ctx may borrow them by pointer. */
static void fo_make_client_masters(xce_client_config_t *conf)
{
static char *hosts[N_MAX_MASTERS] = { "10.30.0.1", "10.30.0.2",
"10.30.0.3" };
static int ports[N_MAX_MASTERS] = { 8001, 8002, 8003 };
memset(conf, 0, sizeof(*conf));
conf->nmasters = N_MAX_MASTERS;
for (int i = 0; i < N_MAX_MASTERS; i++) {
conf->masters[i] = hosts[i];
conf->master_ports[i] = ports[i];
}
}
/* Minimal api_ctx for target-selection tests: only conf, the failover cursor,
* and the ctx redirect-hint fields are exercised. No endpoint/socket touched. */
static void fo_make_api_ctx(api_ctx_t *ac, xce_client_config_t *conf)
{
memset(ac, 0, sizeof(*ac));
ac->conf = conf;
ac->failover.cur_master = 0;
}
static void fo_set_hint(api_ctx_t *ac, const char *host, uint16_t port)
{
strncpy(ac->ctx.redirect_hint_host, host, XCE_MAXHOSTNAME_LEN - 1);
ac->ctx.redirect_hint_host[XCE_MAXHOSTNAME_LEN - 1] = '\0';
ac->ctx.redirect_hint_port = port;
}
/* Req 2.2, 8.4: with a redirect hint set, next_target returns the hint
* host/port and CONSUMES (clears) it; the following call ignores the stale
* hint and advances the master cursor instead. */
static void test_next_target_hint_first_then_consumed(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
fo_set_hint(&ac, "10.99.0.7", 9100);
char host[XCE_MAXHOSTNAME_LEN] = { 0 };
uint16_t port = 0;
int rc = xce_failover_next_target(&ac, host, sizeof(host), &port);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_STRING("10.99.0.7", host);
TEST_ASSERT_EQUAL_INT(9100, port);
/* hint consumed: cleared on the ctx, cursor untouched */
TEST_ASSERT_EQUAL_STRING("", ac.ctx.redirect_hint_host);
TEST_ASSERT_EQUAL_INT(0, ac.failover.cur_master);
/* next call has no hint → advances cursor to master 1 */
rc = xce_failover_next_target(&ac, host, sizeof(host), &port);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_STRING("10.30.0.2", host);
TEST_ASSERT_EQUAL_INT(8002, port);
TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master);
printf("test_next_target_hint_first_then_consumed: PASS\n");
}
/* Req 2.1, 8.4: with no hint, successive next_target calls advance through the
* configured masters and WRAP around; the cursor lives on failover.cur_master. */
static void test_next_target_rotates_and_wraps(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
char host[XCE_MAXHOSTNAME_LEN] = { 0 };
uint16_t port = 0;
/* from cur_master=0, first rotation advances to master 1 */
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.30.0.2", host);
TEST_ASSERT_EQUAL_INT(8002, port);
TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master);
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.30.0.3", host);
TEST_ASSERT_EQUAL_INT(8003, port);
TEST_ASSERT_EQUAL_INT(2, ac.failover.cur_master);
/* wrap back to master 0 */
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.30.0.1", host);
TEST_ASSERT_EQUAL_INT(8001, port);
TEST_ASSERT_EQUAL_INT(0, ac.failover.cur_master);
printf("test_next_target_rotates_and_wraps: PASS\n");
}
/* Req 4.4, 8.4: after a hint is consumed, the very next call advances to the
* next master — correctness is independent of the (one-shot) hint. */
static void test_next_target_hint_then_fallback_rotation(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
/* start the cursor at master 1 to prove fallback advances from current */
ac.failover.cur_master = 1;
fo_set_hint(&ac, "10.99.0.9", 9200);
char host[XCE_MAXHOSTNAME_LEN] = { 0 };
uint16_t port = 0;
/* hint first */
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.99.0.9", host);
TEST_ASSERT_EQUAL_INT(9200, port);
TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master); /* cursor untouched */
/* fallback: advances from master 1 → master 2 */
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.30.0.3", host);
TEST_ASSERT_EQUAL_INT(8003, port);
TEST_ASSERT_EQUAL_INT(2, ac.failover.cur_master);
printf("test_next_target_hint_then_fallback_rotation: PASS\n");
}
/* Guard: NULL/invalid arguments are rejected without touching state. */
static void test_next_target_rejects_bad_args(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
char host[XCE_MAXHOSTNAME_LEN] = { 0 };
uint16_t port = 0;
TEST_ASSERT_EQUAL_INT(-EINVAL,
xce_failover_next_target(NULL, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_INT(-EINVAL,
xce_failover_next_target(&ac, NULL, sizeof(host),
&port));
TEST_ASSERT_EQUAL_INT(-EINVAL,
xce_failover_next_target(&ac, host, 0, &port));
TEST_ASSERT_EQUAL_INT(-EINVAL,
xce_failover_next_target(&ac, host, sizeof(host),
NULL));
printf("test_next_target_rejects_bad_args: PASS\n");
}
/* ── Suite entry point ──────────────────────────────────────────────── */
void test_libxce_retry(void)
@ -467,5 +634,9 @@ void test_libxce_retry(void)
RUN_TEST(test_classify_dup_replay_rule);
RUN_TEST(test_classify_zero_is_success);
RUN_TEST(test_classify_unknown_is_terminal);
RUN_TEST(test_next_target_hint_first_then_consumed);
RUN_TEST(test_next_target_rotates_and_wraps);
RUN_TEST(test_next_target_hint_then_fallback_rotation);
RUN_TEST(test_next_target_rejects_bad_args);
UNITY_END();
}