refactor(ha): passive standby model — primary connects outbound to all peers
Remove fetch_config_from_primary, ha_find_and_connect_to_primary, and the tier-1/2/3 discovery path. Standbys never initiate outbound connections to the primary; the primary owns all inter-controller channels. Key changes: - ha.c: add primary heartbeat loop (ha_start_primary_heartbeat) that sends OPCode_nop to connected standbys every heartbeat_interval_ms; loop self-cancels on demotion via role check before re-arm. - ha.c: add outbound reconnect in primary_heartbeat_fn — for peers absent from standby_endpoints, open fresh outbound TLS connection (on_standby_connected sends ctrlhello; standby_ep_ctx_t mirrors vote_ep_ctx_t layout). - ha.c: remove ha_find_and_connect_to_primary + helpers (ha_raw_tcp_connect, ha_tcp_connect_hook); remove election_timeout_fn (superseded by raft_election_timeout_fn); remove fetch_config_from_primary call from ha_elect_evaluate_and_signal STANDBY branch. - cluster.c: OPCode_requestVote denial no longer reuses inbound connection; late joiners are bootstrapped via primary heartbeat outbound connect. - config_repl.c: remove connect_ssl_raw, do_one_config_fetch, fetch_config_from_primary (~250 LOC). - myapp.c: remove my_tag > 0 fetch branch; all standbys wait passively. - ha.h: remove ha_find_and_connect_to_primary declaration + macros + ha_tcp_connect_hook seam; add ha_start_primary_heartbeat declaration. - meson.build: remove test_ha_connect.c (tests deleted function). - controller_test.c: remove test_ha_connect registration. - CLAUDE.md, src/controller/CLAUDE.md: document passive standby model, inbound-vs-outbound connection ownership, async I/O constraint, heartbeat model. - request.h: add xce_request declaration (removes scattered local externs).
This commit is contained in:
parent
842b9a838e
commit
7b39650229
11 changed files with 716 additions and 919 deletions
|
|
@ -37,9 +37,6 @@
|
|||
#include "xce/cluster_def.h"
|
||||
#include "wal_types.h" /* wal_entry_hdr_t, WAL_* constants */
|
||||
|
||||
/* xce_request: defined in request.c, not yet in any public header */
|
||||
extern int xce_request(endpoint_t *ep, xce_request_t *req);
|
||||
|
||||
/* SSL context for xce_probe_config_tag.
|
||||
* Stored as void* to avoid SSL_CTX type at this point in the translation unit. */
|
||||
static void *s_probe_ssl_ctx = NULL;
|
||||
|
|
@ -678,320 +675,6 @@ int handle_config_request(cluster_t *c, endpoint_t *ep, xce_request_t *req)
|
|||
return rc2;
|
||||
}
|
||||
|
||||
/* ── fetch_config_from_primary helpers ─────────────────────── */
|
||||
|
||||
/*
|
||||
* connect_ssl_raw: open a non-blocking TCP connection to host:port, then
|
||||
* perform a TLS handshake using ssl_ctx.
|
||||
*
|
||||
* The poll loop checks ha_is_primary(ha) between iterations: if this node
|
||||
* is promoted to primary while waiting, the function returns -ECANCELED so
|
||||
* the caller can switch to the primary path immediately.
|
||||
*
|
||||
* On success: *out_ssl and *out_sock are set; caller owns them and must call
|
||||
* SSL_shutdown(ssl) + SSL_free(ssl) + close(sock).
|
||||
* On failure: nothing is left open; returns negative errno.
|
||||
*/
|
||||
static int connect_ssl_raw(SSL_CTX *ssl_ctx, const char *host, uint32_t port,
|
||||
int poll_timeout_ms, const ha_state_t *ha,
|
||||
SSL **out_ssl, int *out_sock)
|
||||
{
|
||||
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), "%u", (unsigned)port);
|
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0)
|
||||
return -ENONET;
|
||||
|
||||
int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (sock < 0) {
|
||||
int e = errno;
|
||||
freeaddrinfo(res);
|
||||
return -e;
|
||||
}
|
||||
|
||||
/* Non-blocking connect */
|
||||
int flags = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
|
||||
connect(sock, res->ai_addr, res->ai_addrlen);
|
||||
freeaddrinfo(res);
|
||||
res = NULL;
|
||||
|
||||
/* Poll loop: check ha_is_primary between iterations */
|
||||
while (!ha_is_primary(ha)) {
|
||||
struct pollfd pfd = { .fd = sock, .events = POLLOUT | POLLERR | POLLHUP };
|
||||
int pr = poll(&pfd, 1, poll_timeout_ms);
|
||||
if (pr < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (pr == 0) {
|
||||
close(sock);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
if (pfd.revents & (POLLERR | POLLHUP)) {
|
||||
close(sock);
|
||||
return -ECONNREFUSED;
|
||||
}
|
||||
if (pfd.revents & POLLOUT)
|
||||
break;
|
||||
}
|
||||
if (ha_is_primary(ha)) {
|
||||
close(sock);
|
||||
return -ECANCELED;
|
||||
}
|
||||
|
||||
/* Verify connect succeeded */
|
||||
{
|
||||
int err = 0;
|
||||
socklen_t el = sizeof(err);
|
||||
getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &el);
|
||||
if (err != 0) {
|
||||
close(sock);
|
||||
return -err;
|
||||
}
|
||||
}
|
||||
|
||||
/* Restore blocking for SSL handshake */
|
||||
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
|
||||
|
||||
SSL *ssl = SSL_new(ssl_ctx);
|
||||
if (!ssl) {
|
||||
close(sock);
|
||||
return -ENOMEM;
|
||||
}
|
||||
SSL_set_fd(ssl, sock);
|
||||
|
||||
int sr;
|
||||
do {
|
||||
sr = SSL_connect(ssl);
|
||||
if (sr <= 0) {
|
||||
int e = SSL_get_error(ssl, sr);
|
||||
if (e != SSL_ERROR_WANT_READ && e != SSL_ERROR_WANT_WRITE)
|
||||
break;
|
||||
}
|
||||
} while (sr <= 0 && !ha_is_primary(ha));
|
||||
|
||||
if (sr <= 0 || ha_is_primary(ha)) {
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return -ECONNRESET;
|
||||
}
|
||||
|
||||
*out_ssl = ssl;
|
||||
*out_sock = sock;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* do_one_config_fetch: attempt one network fetch of the ClusterConfig from
|
||||
* the primary controller over a standalone SSL socket.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Open non-blocking TCP + SSL handshake via connect_ssl_raw()
|
||||
* 2. Send OP_replConfig request with our current version tag
|
||||
* 3. Decode the reply
|
||||
* 4. If up_to_date: nothing to save — return 0
|
||||
* 5. Verify SHA-256 integrity of received bytes
|
||||
* 6. Save to disk via conf_save_config()
|
||||
* 7. Reload cluster_config via load_primary_config()
|
||||
* 8. Log success and clean up
|
||||
*
|
||||
* Returns 0 on success (config up-to-date or freshly saved+loaded).
|
||||
* Returns -ECANCELED if promoted to primary during the attempt (caller should
|
||||
* switch to primary path). Returns other negative errno on transient failure
|
||||
* (caller will retry with backoff).
|
||||
*/
|
||||
static int do_one_config_fetch(cluster_t *c, const char *host, uint32_t port,
|
||||
uint64_t our_tag)
|
||||
{
|
||||
SSL *ssl = NULL;
|
||||
int sock = -1;
|
||||
|
||||
/* 1. Connect + SSL handshake — use client_ssl_ctx (TLS_client_method) for
|
||||
* outgoing connections; ssl_ctx is the server context (TLS_server_method). */
|
||||
SSL_CTX *out_ctx = c->ha.client_ssl_ctx
|
||||
? (SSL_CTX *)c->ha.client_ssl_ctx
|
||||
: (SSL_CTX *)c->ha.ssl_ctx;
|
||||
int rc = connect_ssl_raw(
|
||||
out_ctx, host, port,
|
||||
(int)c->ha.config.probe_connect_timeout_ms,
|
||||
&c->ha, &ssl, &sock);
|
||||
if (rc != 0)
|
||||
return rc;
|
||||
|
||||
/* 2. Send OP_replConfig request */
|
||||
rc = xce_replconfig_req_ssl(ssl, our_tag);
|
||||
if (rc != 0) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 3. Decode reply */
|
||||
int up_to_date = 0;
|
||||
uint64_t new_tag = 0;
|
||||
uint8_t *sha256 = NULL;
|
||||
uint8_t *config_bytes = NULL;
|
||||
size_t config_len = 0;
|
||||
|
||||
rc = xce_replconfig_reply_ssl_decode(ssl, &up_to_date, &new_tag,
|
||||
&sha256, &config_bytes, &config_len);
|
||||
if (rc != 0) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 4. Up-to-date fast path: our tag matches primary — nothing to save */
|
||||
if (up_to_date) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 5. Verify SHA-256 integrity */
|
||||
uint8_t computed[32];
|
||||
SHA256(config_bytes, config_len, computed);
|
||||
if (memcmp(computed, sha256, 32) != 0) {
|
||||
ERROR("HA: config fetch SHA-256 mismatch from primary — retrying");
|
||||
free(sha256);
|
||||
free(config_bytes);
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
/* 6. Save to disk atomically */
|
||||
rc = conf_save_config(c->myconf, config_bytes, config_len, sha256);
|
||||
free(sha256);
|
||||
free(config_bytes);
|
||||
sha256 = NULL;
|
||||
config_bytes = NULL;
|
||||
if (rc != 0) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 7. Reload cluster_config from the newly saved file */
|
||||
rc = load_primary_config(c);
|
||||
if (rc != 0) {
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 8. Log success */
|
||||
INFO("HA: config received from primary (tag=%016" PRIx64 ")",
|
||||
c->myconf->config_version_tag);
|
||||
|
||||
/* Clean up SSL and socket */
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
close(sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── fetch_config_from_primary ──────────────────── */
|
||||
|
||||
int fetch_config_from_primary(cluster_t *c)
|
||||
{
|
||||
const xce_cluster_config_t *pcfg = &c->myconf->peers_cfg;
|
||||
|
||||
/* Build conf dir path: <workdir>/<name> */
|
||||
char conf_dir[4096];
|
||||
snprintf(conf_dir, sizeof(conf_dir), "%s/%s",
|
||||
c->myconf->workdir, c->myconf->name);
|
||||
|
||||
uint64_t saved_tag = conf_get_saved_tag(conf_dir);
|
||||
|
||||
if (pcfg->candidates_count == 0) {
|
||||
/* Single-node mode — no primary to contact; load from saved file */
|
||||
goto use_saved;
|
||||
}
|
||||
|
||||
{
|
||||
/* Primary is candidates[0] (highest priority = lowest index) */
|
||||
const char *primary_host = pcfg->candidates[0].hostname;
|
||||
uint32_t primary_port = pcfg->candidates[0].port;
|
||||
|
||||
INFO("HA: requesting config from primary %s:%u (my_tag=%016" PRIx64 ")",
|
||||
primary_host, primary_port, saved_tag);
|
||||
|
||||
uint32_t delay_ms = 500;
|
||||
const uint32_t max_delay_ms = 5000;
|
||||
const int max_attempts = 60; /* up to 5min: primary may still be initializing */
|
||||
|
||||
for (int attempt = 0; attempt < max_attempts; attempt++) {
|
||||
/* If promoted during retry, switch to primary path */
|
||||
if (ha_is_primary(&c->ha)) {
|
||||
INFO("HA: promoted to primary during config fetch"
|
||||
" — switching to primary path");
|
||||
return load_primary_config(c);
|
||||
}
|
||||
|
||||
int rc = do_one_config_fetch(c, primary_host, primary_port,
|
||||
saved_tag);
|
||||
if (rc == 0) {
|
||||
INFO("HA: config received from primary"
|
||||
" (tag=%016" PRIx64 ")",
|
||||
c->myconf->config_version_tag);
|
||||
/* Config loaded — complete standby bootstrap:
|
||||
* 1. cluster_init_phase2 to set up data structures with config
|
||||
* 2. ha_find_and_connect_to_primary to establish the persistent
|
||||
* control channel needed for RDHUP failover detection */
|
||||
#ifndef XCE_TESTING
|
||||
{
|
||||
extern int cluster_init_phase2(cluster_t *c);
|
||||
int p2rc = cluster_init_phase2(c);
|
||||
if (p2rc == 0) {
|
||||
ha_find_and_connect_to_primary(&c->ha, &c->ha.config,
|
||||
&c->myconf->peers_cfg, c);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (attempt < max_attempts - 1) {
|
||||
WARN("HA: config fetch attempt %d/%d failed (rc=%d)"
|
||||
" — retrying in %ums",
|
||||
attempt + 1, max_attempts, rc, delay_ms);
|
||||
usleep((useconds_t)delay_ms * 1000);
|
||||
delay_ms = delay_ms * 2 < max_delay_ms
|
||||
? delay_ms * 2 : max_delay_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use_saved:
|
||||
{
|
||||
char fname[256] = {0};
|
||||
int rc = conf_read_current_filename(conf_dir, fname, sizeof(fname));
|
||||
if (rc != 0) {
|
||||
ERROR("Standby (priority=%d) could not fetch config from primary"
|
||||
" and has no saved config at '%s/conf/current'"
|
||||
" — start the primary first or provision config manually",
|
||||
c->ha.my_priority, conf_dir);
|
||||
return -ENOENT;
|
||||
}
|
||||
WARN("HA: using saved config (tag=%016" PRIx64 ")"
|
||||
" — config may be stale", saved_tag);
|
||||
/* load_primary_config resolves conf/current → versioned file,
|
||||
* decodes Cap'n Proto, computes SHA-256, and checks staleness.
|
||||
* It is reused here since the saved-config load path is identical. */
|
||||
return load_primary_config(c);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── send_upgrade_error ─────────────────────────── */
|
||||
|
||||
static void send_upgrade_error(endpoint_t *ep, xce_request_t *req,
|
||||
|
|
@ -1399,43 +1082,58 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out)
|
|||
int sock = -1;
|
||||
int flags_saved = 0;
|
||||
|
||||
/* ── Step 1: SSL connect via connect_ssl_raw (same path as do_one_config_fetch) ── */
|
||||
/* ── Step 1: TCP connect (+ optional SSL handshake) ── */
|
||||
{
|
||||
/* s_ssl_ctx may be set externally; if NULL (unit test), fall back to plain TCP. */
|
||||
SSL_CTX *ssl_ctx = s_probe_ssl_ctx;
|
||||
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), "%u", (unsigned)port);
|
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0)
|
||||
return -EHOSTUNREACH;
|
||||
|
||||
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (sock < 0) { int e = errno; freeaddrinfo(res); return -e; }
|
||||
|
||||
flags_saved = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags_saved | 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, 500); } 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_saved & ~O_NONBLOCK);
|
||||
|
||||
/* Optional SSL handshake — only when s_probe_ssl_ctx is set */
|
||||
SSL_CTX *ssl_ctx = (SSL_CTX *)s_probe_ssl_ctx;
|
||||
if (ssl_ctx) {
|
||||
int rc = connect_ssl_raw(ssl_ctx, host, port, 500, NULL, &ssl, &sock);
|
||||
if (rc != 0)
|
||||
return rc;
|
||||
} else {
|
||||
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), "%u", (unsigned)port);
|
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0)
|
||||
return -EHOSTUNREACH;
|
||||
|
||||
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (sock < 0) { int e = errno; freeaddrinfo(res); return -e; }
|
||||
|
||||
flags_saved = fcntl(sock, F_GETFL, 0);
|
||||
fcntl(sock, F_SETFL, flags_saved | 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, 500); } 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_saved & ~O_NONBLOCK);
|
||||
ssl = SSL_new(ssl_ctx);
|
||||
if (!ssl) { close(sock); return -ENOMEM; }
|
||||
SSL_set_fd(ssl, sock);
|
||||
int sr;
|
||||
do {
|
||||
sr = SSL_connect(ssl);
|
||||
if (sr <= 0) {
|
||||
int ssl_err = SSL_get_error(ssl, sr);
|
||||
if (ssl_err != SSL_ERROR_WANT_READ &&
|
||||
ssl_err != SSL_ERROR_WANT_WRITE)
|
||||
break;
|
||||
}
|
||||
} while (sr <= 0);
|
||||
if (sr <= 0) {
|
||||
SSL_free(ssl);
|
||||
ssl = NULL;
|
||||
close(sock);
|
||||
return -ECONNREFUSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1472,11 +1170,18 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out)
|
|||
/* Wire framing: 8-byte size prefix then capnp payload (matches ha.c pattern) */
|
||||
uint64_t hdr = (uint64_t)blen;
|
||||
ssize_t nsent;
|
||||
nsent = (ssize_t)write(sock, &hdr, sizeof(hdr));
|
||||
if (nsent == (ssize_t)sizeof(hdr))
|
||||
nsent = (ssize_t)write(sock, buf, (size_t)blen);
|
||||
if (ssl) {
|
||||
nsent = (ssize_t)SSL_write(ssl, &hdr, (int)sizeof(hdr));
|
||||
if (nsent == (ssize_t)sizeof(hdr))
|
||||
nsent = (ssize_t)SSL_write(ssl, buf, (int)blen);
|
||||
} else {
|
||||
nsent = (ssize_t)write(sock, &hdr, sizeof(hdr));
|
||||
if (nsent == (ssize_t)sizeof(hdr))
|
||||
nsent = (ssize_t)write(sock, buf, (size_t)blen);
|
||||
}
|
||||
free(buf);
|
||||
if (nsent != (ssize_t)blen) {
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
return -EIO;
|
||||
}
|
||||
|
|
@ -1489,8 +1194,10 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out)
|
|||
|
||||
/* Read 8-byte size header */
|
||||
while (off < sizeof(hdr)) {
|
||||
int n = (int)read(sock, (char *)&hdr + off, sizeof(hdr) - off);
|
||||
int n = ssl ? SSL_read(ssl, (char *)&hdr + off, (int)(sizeof(hdr) - off))
|
||||
: (int)read(sock, (char *)&hdr + off, sizeof(hdr) - off);
|
||||
if (n <= 0) {
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
return -EIO;
|
||||
}
|
||||
|
|
@ -1498,21 +1205,25 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out)
|
|||
}
|
||||
|
||||
if (hdr == 0 || hdr > 65536) { /* sanity cap — reply is tiny */
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
return -EMSGSIZE;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc((size_t)hdr);
|
||||
if (!buf) {
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
off = 0;
|
||||
while (off < (size_t)hdr) {
|
||||
int n = (int)read(sock, buf + off, (size_t)hdr - off);
|
||||
int n = ssl ? SSL_read(ssl, buf + off, (int)((size_t)hdr - off))
|
||||
: (int)read(sock, buf + off, (size_t)hdr - off);
|
||||
if (n <= 0) {
|
||||
free(buf);
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
return -EIO;
|
||||
}
|
||||
|
|
@ -1536,6 +1247,7 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out)
|
|||
*tag_out = rep_s.configTag;
|
||||
}
|
||||
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
close(sock);
|
||||
INFO("HA: probe config tag from %s:%u tag=%016" PRIx64,
|
||||
host, (unsigned)port, *tag_out);
|
||||
|
|
@ -1590,6 +1302,18 @@ int handle_ctrlhello(cluster_t *c, endpoint_t *ep, xce_request_t *req)
|
|||
uint64_t standby_applied_lsn = hello->applied_lsn;
|
||||
uint64_t standby_config_tag = hello->config_tag;
|
||||
|
||||
/* Req 3.1: on the standby side, the first ctrlhello from the winner
|
||||
* designates this inbound connection as the primary control channel.
|
||||
* Guard with primary_ctrl_fd == -1 for idempotency. */
|
||||
if (c && ep &&
|
||||
(ctrl_role_t)atomic_load_explicit(&c->ha.role,
|
||||
memory_order_acquire) == CTRL_STANDBY &&
|
||||
c->ha.primary_ctrl_fd == -1) {
|
||||
c->ha.primary_ctrl_fd = ep->fd;
|
||||
ha_on_heartbeat_received(&c->ha);
|
||||
INFO("HA: primary control channel established (fd=%d)", ep->fd);
|
||||
}
|
||||
|
||||
/* 2. Register standby in replication set and mark as catching up */
|
||||
int rc = repl_add_standby(&c->persist.repl, ep,
|
||||
hello->name ? hello->name : "unknown", 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue