feat(ha-config-bootstrap): implement ha_find_and_connect_to_primary three-tier discovery (task 9.2)

This commit is contained in:
Rongsong Shen 2026-05-28 15:09:08 +00:00
parent 3252c89e7a
commit 97a76617e6
6 changed files with 428 additions and 1 deletions

View file

@ -13,6 +13,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
@ -1362,3 +1363,164 @@ ctrl_role_t am_i_primary_quorum(ha_state_t *ha,
return CTRL_STANDBY;
}
/* ── Task 9.2: Persistent Control Channel ──────────────────────────────────
*
* ha_find_and_connect_to_primary: three-tier primary discovery + TCP connect.
*
* Requirements: 6.1
* Boundary: Persistent Control Channel (ha.c)
*/
#ifdef XCE_TESTING
int (*ha_tcp_connect_hook)(const char *host, int port) = NULL;
#endif
/* ha_tcp_connect_raw: non-blocking connect + poll, returns connected fd or -errno */
static int ha_tcp_connect_raw(const char *host, int port, int timeout_ms)
{
#ifdef XCE_TESTING
if (ha_tcp_connect_hook)
return ha_tcp_connect_hook(host, port);
#endif
struct addrinfo hints, *res = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0)
return -EHOSTUNREACH;
int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock < 0) { int e = errno; freeaddrinfo(res); return -e; }
int one = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
connect(sock, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
struct pollfd pfd = { .fd = sock, .events = POLLOUT | POLLERR | POLLHUP };
int pr;
do { pr = poll(&pfd, 1, timeout_ms); } while (pr < 0 && errno == EINTR);
if (pr == 0) { close(sock); return -ETIMEDOUT; }
if (pr < 0 || (pfd.revents & (POLLERR | POLLHUP))) {
close(sock); return -ECONNREFUSED;
}
int err = 0; socklen_t el = sizeof(err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &el);
if (err) { close(sock); return -err; }
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
return sock;
}
static int find_candidate_by_name(const xce_cluster_config_t *peers,
const char *name)
{
for (int i = 0; i < peers->candidates_count; i++) {
if (strcmp(peers->candidates[i].name, name) == 0)
return i;
}
return -1;
}
static void register_fd_with_iomgr(cluster_t *c, int fd)
{
if (!c) return;
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.events = EPOLLIN | EPOLLRDHUP;
ev.data.fd = fd;
if (epoll_ctl(c->iomgr.epoll_fd, EPOLL_CTL_ADD, fd, &ev) != 0)
WARN("ha_find_and_connect: epoll_ctl: %s", strerror(errno));
}
static void ms_sleep_ha(long delay_ms)
{
if (delay_ms <= 0) return;
struct timespec ts = { .tv_sec = delay_ms / 1000,
.tv_nsec = (delay_ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
int ha_find_and_connect_to_primary(ha_state_t *ha,
const ha_config_t *cfg,
const xce_cluster_config_t *peers,
cluster_t *c)
{
if (!ha || !cfg || !peers || peers->candidates_count <= 0)
return -EINVAL;
int timeout_ms = (int)cfg->probe_connect_timeout_ms;
if (timeout_ms <= 0)
timeout_ms = HA_DEFAULT_PROBE_CONNECT_TIMEOUT_MS;
/* Tier 1: voted_for_name → direct connect */
if (ha->voted_for_name[0] != '\0') {
int idx = find_candidate_by_name(peers, ha->voted_for_name);
if (idx >= 0) {
const xce_candidate_t *p1 = &peers->candidates[idx];
for (int r = 0; r < HA_FIND_PRIMARY_T1_MAX_RETRIES; r++) {
int fd = ha_tcp_connect_raw(p1->hostname, p1->port, timeout_ms);
if (fd >= 0) {
INFO("HA: Tier 1 connected to voted_for '%s' (retry=%d)",
ha->voted_for_name, r);
ha->primary_ctrl_fd = fd;
register_fd_with_iomgr(c, fd);
return fd;
}
if (fd == -ETIMEDOUT || fd == -EAGAIN) {
WARN("HA: Tier 1 '%s' not ready (retry %d/%d), backoff %dms",
ha->voted_for_name, r + 1,
HA_FIND_PRIMARY_T1_MAX_RETRIES, HA_FIND_PRIMARY_T1_BACKOFF_MS);
ms_sleep_ha(HA_FIND_PRIMARY_T1_BACKOFF_MS);
continue;
}
break; /* other errors → Tier 2 */
}
}
}
/* Tier 2: probe remaining candidates */
{
int t1_idx = find_candidate_by_name(peers, ha->voted_for_name);
for (int i = 0; i < peers->candidates_count; i++) {
if (i == t1_idx) continue;
const xce_candidate_t *p2 = &peers->candidates[i];
int fd = ha_tcp_connect_raw(p2->hostname, p2->port, timeout_ms);
if (fd >= 0) {
INFO("HA: Tier 2 connected to '%s'", p2->name);
ha->primary_ctrl_fd = fd;
register_fd_with_iomgr(c, fd);
return fd;
}
}
}
/* Tier 3: exponential backoff scan (no exit) */
INFO("HA: entering Tier 3 primary discovery");
long backoff_ms = HA_FIND_PRIMARY_T3_INIT_MS;
for (;;) {
for (int i = 0; i < peers->candidates_count; i++) {
const xce_candidate_t *p3 = &peers->candidates[i];
int fd = ha_tcp_connect_raw(p3->hostname, p3->port, timeout_ms);
if (fd >= 0) {
INFO("HA: Tier 3 connected to '%s' after backoff", p3->name);
ha->primary_ctrl_fd = fd;
register_fd_with_iomgr(c, fd);
return fd;
}
}
WARN("HA: Tier 3 — no primary, sleep %ldms", backoff_ms);
ms_sleep_ha(backoff_ms);
backoff_ms = backoff_ms * 2 > HA_FIND_PRIMARY_T3_MAX_MS
? HA_FIND_PRIMARY_T3_MAX_MS : backoff_ms * 2;
}
}