feat(controller-peers-env-fallback): route the controller's two identity loops through the shared lookup (task 2.1)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
241ed3a142
commit
7a90af089c
3 changed files with 145 additions and 22 deletions
|
|
@ -22,7 +22,7 @@
|
||||||
- Done when, after `/xce-build` (controller, scheduler, worker, harness) on a machine with no local cluster, the script runs with no cases and exits 0, and exits non-zero with the pre-flight message while a listener is bound to port 9990 or when pointed at a build directory missing a binary
|
- Done when, after `/xce-build` (controller, scheduler, worker, harness) on a machine with no local cluster, the script runs with no cases and exits 0, and exits non-zero with the pre-flight message while a listener is bound to port 9990 or when pointed at a build directory missing a binary
|
||||||
|
|
||||||
- [ ] 2. Controller
|
- [ ] 2. Controller
|
||||||
- [ ] 2.1 Route the controller's two existing identity loops through the shared lookup
|
- [x] 2.1 Route the controller's two existing identity loops through the shared lookup
|
||||||
- Replace the loop that picks the controller's election priority, keeping the existing "at least one candidate" guard so a standalone controller still gets priority 0, and keeping "no match gives the last index" at that call site
|
- Replace the loop that picks the controller's election priority, keeping the existing "at least one candidate" guard so a standalone controller still gets priority 0, and keeping "no match gives the last index" at that call site
|
||||||
- Replace the loop that picks the listen port, keeping the existing "no match with a peers list refuses to start" and "port 0 falls back to the default port" behavior
|
- Replace the loop that picks the listen port, keeping the existing "no match with a peers list refuses to start" and "port 0 falls back to the default port" behavior
|
||||||
- Pure refactor: no behavior change for any input
|
- Pure refactor: no behavior change for any input
|
||||||
|
|
|
||||||
|
|
@ -1780,16 +1780,9 @@ int cluster_init_phase1(cluster_t* c)
|
||||||
int my_priority = 0;
|
int my_priority = 0;
|
||||||
const xce_cluster_config_t* pcfg = &c->myconf->peers_cfg;
|
const xce_cluster_config_t* pcfg = &c->myconf->peers_cfg;
|
||||||
if (pcfg->candidates_count > 0) {
|
if (pcfg->candidates_count > 0) {
|
||||||
my_priority = pcfg->candidates_count - 1; /* default: last = lowest */
|
/* No match: last index = lowest priority. */
|
||||||
for (int i = 0; i < pcfg->candidates_count; i++) {
|
int idx = xce_peers_find_identity(pcfg, c->myconf->name);
|
||||||
const char* key = pcfg->candidates[i].name[0]
|
my_priority = idx >= 0 ? idx : pcfg->candidates_count - 1;
|
||||||
? pcfg->candidates[i].name
|
|
||||||
: pcfg->candidates[i].hostname;
|
|
||||||
if (strncmp(key, c->myconf->name, XCE_MAXNAME_LEN - 1) == 0) {
|
|
||||||
my_priority = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
INFO("HA peers: %d candidates, my_priority=%d (name=%s)",
|
INFO("HA peers: %d candidates, my_priority=%d (name=%s)",
|
||||||
pcfg->candidates_count, my_priority, c->myconf->name);
|
pcfg->candidates_count, my_priority, c->myconf->name);
|
||||||
}
|
}
|
||||||
|
|
@ -1873,7 +1866,8 @@ int cluster_open_listener(cluster_t* c)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
/* Determine listen port:
|
/* Determine listen port:
|
||||||
* 1. Search --peers candidates for the local name.
|
* 1. Look up the local name among the --peers candidates
|
||||||
|
* (xce_peers_find_identity()).
|
||||||
* - Matched with a nonzero port: bind to it (unchanged happy
|
* - Matched with a nonzero port: bind to it (unchanged happy
|
||||||
* path; also how a single-node operator gets a custom port,
|
* path; also how a single-node operator gets a custom port,
|
||||||
* via a single self-referencing --peers entry).
|
* via a single self-referencing --peers entry).
|
||||||
|
|
@ -1889,16 +1883,9 @@ int cluster_open_listener(cluster_t* c)
|
||||||
bool matched = false;
|
bool matched = false;
|
||||||
{
|
{
|
||||||
const xce_cluster_config_t* pcfg = &c->myconf->peers_cfg;
|
const xce_cluster_config_t* pcfg = &c->myconf->peers_cfg;
|
||||||
for (int j = 0; j < pcfg->candidates_count; j++) {
|
int idx = xce_peers_find_identity(pcfg, c->myconf->name);
|
||||||
const char* key = pcfg->candidates[j].name[0]
|
matched = idx >= 0;
|
||||||
? pcfg->candidates[j].name
|
my_port = matched ? (uint32_t)pcfg->candidates[idx].port : 0;
|
||||||
: pcfg->candidates[j].hostname;
|
|
||||||
if (strncmp(key, c->myconf->name, XCE_MAXNAME_LEN - 1) == 0) {
|
|
||||||
my_port = pcfg->candidates[j].port;
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched && pcfg->candidates_count > 0) {
|
if (!matched && pcfg->candidates_count > 0) {
|
||||||
ERROR("cluster_open_listener: local name '%s' not found in "
|
ERROR("cluster_open_listener: local name '%s' not found in "
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,19 @@
|
||||||
* Instead they manually zero-init cluster_t and call ha_init() on c.ha directly.
|
* Instead they manually zero-init cluster_t and call ha_init() on c.ha directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <unity/unity.h>
|
#include <unity/unity.h>
|
||||||
|
|
||||||
|
#include "cluster.h"
|
||||||
#include "ha.h"
|
#include "ha.h"
|
||||||
#include "xce/cluster_def.h"
|
#include "xce/cluster_def.h"
|
||||||
|
|
||||||
|
|
@ -138,6 +146,130 @@ static void test_hooks_registered_guard_prevents_double_register(void)
|
||||||
printf("test_hooks_registered_guard_prevents_double_register: PASS\n");
|
printf("test_hooks_registered_guard_prevents_double_register: PASS\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── cluster_open_listener(): which --peers candidate is this controller ──
|
||||||
|
*
|
||||||
|
* Characterization tests (controller-peers-env-fallback task 2.1): pin the
|
||||||
|
* listener's candidate match before it is routed through
|
||||||
|
* xce_peers_find_identity(). The key is the candidate's name, or its
|
||||||
|
* hostname when unnamed; a match binds that candidate's port, and no match
|
||||||
|
* with a non-empty --peers list refuses to start without opening a socket.
|
||||||
|
* The port-0/standalone default-port fallback is left out: it binds a fixed
|
||||||
|
* port (9990) that another process on this host may hold. */
|
||||||
|
|
||||||
|
static void listener_set_candidate(xce_cluster_config_t* cfg, int i,
|
||||||
|
const char* name, const char* hostname,
|
||||||
|
int port)
|
||||||
|
{
|
||||||
|
snprintf(cfg->candidates[i].name, XCE_MAXNAME_LEN, "%s", name);
|
||||||
|
snprintf(cfg->candidates[i].hostname, XCE_MAXHOSTNAME_LEN, "%s",
|
||||||
|
hostname);
|
||||||
|
cfg->candidates[i].port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A zeroed cluster_t with only what cluster_open_listener() reads. */
|
||||||
|
static cluster_t* listener_make_cluster(myconf_t* conf, const char* name)
|
||||||
|
{
|
||||||
|
cluster_t* c = calloc(1, sizeof(*c));
|
||||||
|
TEST_ASSERT_NOT_NULL(c);
|
||||||
|
memset(conf, 0, sizeof(*conf));
|
||||||
|
conf->name = (char*)name;
|
||||||
|
c->myconf = conf;
|
||||||
|
c->myhost = &c->myhost_info;
|
||||||
|
c->ep.fd = -1;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An unused TCP port: bind port 0, read it back, release it. */
|
||||||
|
static int listener_free_port(void)
|
||||||
|
{
|
||||||
|
struct sockaddr_in sa;
|
||||||
|
socklen_t len = sizeof(sa);
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
TEST_ASSERT_TRUE(fd >= 0);
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sin_family = AF_INET;
|
||||||
|
sa.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
sa.sin_port = 0;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, bind(fd, (struct sockaddr*)&sa, sizeof(sa)));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, getsockname(fd, (struct sockaddr*)&sa, &len));
|
||||||
|
close(fd);
|
||||||
|
return ntohs(sa.sin_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int listener_bound_port(int fd)
|
||||||
|
{
|
||||||
|
struct sockaddr_in sa;
|
||||||
|
socklen_t len = sizeof(sa);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, getsockname(fd, (struct sockaddr*)&sa, &len));
|
||||||
|
return ntohs(sa.sin_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_open_listener_binds_named_candidate_port(void)
|
||||||
|
{
|
||||||
|
myconf_t conf;
|
||||||
|
cluster_t* c = listener_make_cluster(&conf, "ctrl-b");
|
||||||
|
int port = listener_free_port();
|
||||||
|
|
||||||
|
conf.peers_cfg.candidates_count = 2;
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 0, "ctrl-a", "localhost", 1);
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 1, "ctrl-b", "localhost", port);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, cluster_open_listener(c));
|
||||||
|
TEST_ASSERT_TRUE(c->ep.fd >= 0);
|
||||||
|
TEST_ASSERT_EQUAL_INT(port, listener_bound_port(c->ep.fd));
|
||||||
|
|
||||||
|
close(c->ep.fd);
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_open_listener_matches_unnamed_candidate_by_hostname(void)
|
||||||
|
{
|
||||||
|
myconf_t conf;
|
||||||
|
cluster_t* c = listener_make_cluster(&conf, "node7");
|
||||||
|
int port = listener_free_port();
|
||||||
|
|
||||||
|
conf.peers_cfg.candidates_count = 2;
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 0, "ctrl-a", "localhost", 1);
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 1, "", "node7", port);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, cluster_open_listener(c));
|
||||||
|
TEST_ASSERT_TRUE(c->ep.fd >= 0);
|
||||||
|
TEST_ASSERT_EQUAL_INT(port, listener_bound_port(c->ep.fd));
|
||||||
|
|
||||||
|
close(c->ep.fd);
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_open_listener_refuses_when_name_not_in_peers(void)
|
||||||
|
{
|
||||||
|
myconf_t conf;
|
||||||
|
cluster_t* c = listener_make_cluster(&conf, "ctrl-z");
|
||||||
|
|
||||||
|
conf.peers_cfg.candidates_count = 2;
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 0, "ctrl-a", "localhost", 1);
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 1, "ctrl-b", "localhost", 2);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, cluster_open_listener(c));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, c->ep.fd);
|
||||||
|
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A named candidate is never matched by its hostname. */
|
||||||
|
static void test_open_listener_ignores_hostname_of_named_candidate(void)
|
||||||
|
{
|
||||||
|
myconf_t conf;
|
||||||
|
cluster_t* c = listener_make_cluster(&conf, "node7");
|
||||||
|
|
||||||
|
conf.peers_cfg.candidates_count = 1;
|
||||||
|
listener_set_candidate(&conf.peers_cfg, 0, "ctrl-a", "node7", 1);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, cluster_open_listener(c));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, c->ep.fd);
|
||||||
|
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
void test_ha_cluster(void)
|
void test_ha_cluster(void)
|
||||||
{
|
{
|
||||||
UnitySetTestFile(__FILE__);
|
UnitySetTestFile(__FILE__);
|
||||||
|
|
@ -146,4 +278,8 @@ void test_ha_cluster(void)
|
||||||
RUN_TEST(test_ha_demote_to_standby_uses_c_ha);
|
RUN_TEST(test_ha_demote_to_standby_uses_c_ha);
|
||||||
RUN_TEST(test_ha_promote_increments_term_via_cluster);
|
RUN_TEST(test_ha_promote_increments_term_via_cluster);
|
||||||
RUN_TEST(test_hooks_registered_guard_prevents_double_register);
|
RUN_TEST(test_hooks_registered_guard_prevents_double_register);
|
||||||
|
RUN_TEST(test_open_listener_binds_named_candidate_port);
|
||||||
|
RUN_TEST(test_open_listener_matches_unnamed_candidate_by_hostname);
|
||||||
|
RUN_TEST(test_open_listener_refuses_when_name_not_in_peers);
|
||||||
|
RUN_TEST(test_open_listener_ignores_hostname_of_named_candidate);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue