feat(component-failover-adoption): reconcile N_MAX_MASTERS with MAX_HA_CANDIDATES

N_MAX_MASTERS (client.h) now derives from config.h's MAX_HA_CANDIDATES
instead of an independently-maintained literal (3 vs 4 previously).
Fixes worker's and scheduler's hardcoded `> 3` startup checks to
reference the constant symbolically, and updates the client-retry
test fixture so it doesn't exercise an uninitialized 4th slot.

Task 1.1 of component-failover-adoption (Requirement 1 AC7).
This commit is contained in:
Rongsong Shen 2026-08-11 04:31:53 +00:00
parent 0df1cf5f8d
commit fe8b4b86bf
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
5 changed files with 174 additions and 93 deletions

View file

@ -6,6 +6,7 @@
#include <stddef.h>
#include <stdint.h>
#include "config.h"
#include "netio.h"
#include "request.h"
#include "xce/types.h"
@ -25,7 +26,9 @@ typedef void* xcecli_ctx_t;
*/
typedef int (*xcecli_reconnect_cb)(xcecli_ctx_t ctx, void* arg);
#define N_MAX_MASTERS (3)
/* N_MAX_MASTERS derives from config.h's MAX_HA_CANDIDATES so the two limits
* can never silently diverge (Req 1 AC7, component-failover-adoption). */
#define N_MAX_MASTERS MAX_HA_CANDIDATES
typedef struct {
int nmasters;

View file

@ -37,6 +37,14 @@
#include "xce/client_failover.h"
#include "xce/errnox.h"
/* Req 1.7 (component-failover-adoption): client.h's N_MAX_MASTERS must derive
* from config.h's MAX_HA_CANDIDATES, not be an independently-maintained
* literal that can silently diverge from it (was 3 vs. 4). Compile-time so a
* future re-divergence fails the build, not just a test run. */
_Static_assert(N_MAX_MASTERS == MAX_HA_CANDIDATES,
"N_MAX_MASTERS (client.h) must derive from MAX_HA_CANDIDATES "
"(config.h), not be independently defined");
/* ════════════════════════════════════════════════════════════════════════
* Harness implementation
* ════════════════════════════════════════════════════════════════════════ */
@ -460,13 +468,17 @@ static void test_classify_unknown_is_terminal(void)
* 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. */
/* Build a full N_MAX_MASTERS-candidate client config (distinct host:port) the
* rotator selects over. Strings are static-duration so the api_ctx may borrow
* them by pointer. All N_MAX_MASTERS slots are genuinely initialized with
* literal values -- do NOT let this list fall short of N_MAX_MASTERS entries,
* or the copy loop below silently copies an uninitialized (NULL host, port 0)
* slot once N_MAX_MASTERS is raised (Req 1 AC7, component-failover-adoption). */
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 };
"10.30.0.3", "10.30.0.4" };
static int ports[N_MAX_MASTERS] = { 8001, 8002, 8003, 8004 };
memset(conf, 0, sizeof(*conf));
conf->nmasters = N_MAX_MASTERS;
@ -485,6 +497,27 @@ static void fo_make_api_ctx(api_ctx_t *ac, xce_client_config_t *conf)
ac->failover.cur_master = 0;
}
/* Req 1.7: fo_make_client_masters() must genuinely initialize every one of
* N_MAX_MASTERS slots -- not just leave conf->nmasters == N_MAX_MASTERS while
* silently leaving a trailing slot at its NULL/0 memset default. Guards
* against the exact regression this task fixed: N_MAX_MASTERS raised without
* also extending the fixture's literal host/port list. */
static void test_client_masters_hold_n_max_candidates(void)
{
xce_client_config_t conf;
fo_make_client_masters(&conf);
TEST_ASSERT_EQUAL_INT(N_MAX_MASTERS, conf.nmasters);
for (int i = 0; i < N_MAX_MASTERS; i++) {
TEST_ASSERT_NOT_NULL(conf.masters[i]);
TEST_ASSERT_TRUE(conf.master_ports[i] != 0);
}
TEST_ASSERT_EQUAL_STRING("10.30.0.4", conf.masters[N_MAX_MASTERS - 1]);
TEST_ASSERT_EQUAL_INT(8004, conf.master_ports[N_MAX_MASTERS - 1]);
printf("test_client_masters_hold_n_max_candidates: PASS\n");
}
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);
@ -550,7 +583,14 @@ static void test_next_target_rotates_and_wraps(void)
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.4", host);
TEST_ASSERT_EQUAL_INT(8004, port);
TEST_ASSERT_EQUAL_INT(3, ac.failover.cur_master);
/* wrap back to master 0 once every configured candidate (N_MAX_MASTERS
* of them) has been visited */
TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host),
&port));
TEST_ASSERT_EQUAL_STRING("10.30.0.1", host);
@ -1681,6 +1721,7 @@ 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_client_masters_hold_n_max_candidates);
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);

View file

@ -311,10 +311,10 @@ int main(int argc, char* argv[])
"identity?).\n");
return 1;
}
if (cluster_cfg.candidates_count > 3) {
if (cluster_cfg.candidates_count > N_MAX_MASTERS) {
WARN("xce_scheduler: --peers has %d candidates; "
"only <=3 validated for HA",
cluster_cfg.candidates_count);
"only <=%d validated for HA",
cluster_cfg.candidates_count, N_MAX_MASTERS);
}
INFO("HA cluster config: %d candidate(s), identity=%s",
cluster_cfg.candidates_count, cluster_cfg.identity);

View file

@ -411,11 +411,11 @@ int main(int argc, char* argv[])
"Use -n.\n");
return 1;
}
if (cluster_cfg.candidates_count > 3) {
if (cluster_cfg.candidates_count > N_MAX_MASTERS) {
fprintf(stderr,
"xcecd: WARN: --peers has %d candidates; "
"only <=3 validated for HA\n",
cluster_cfg.candidates_count);
"only <=%d validated for HA\n",
cluster_cfg.candidates_count, N_MAX_MASTERS);
}
XCECD_LOG(XCE_LOG_INFO,
"HA cluster config: %d candidate(s), identity=%s",