feat(controller-status): source per-controller connection status in ctrlstatus_fill (task 8.2)

Populates CtrlPeer.status (added in task 8.1) inside ctrlstatus_fill's
peer-building loop: self always reports ok; a primary responder sources
every other peer from the live replication connection registry
(replicator_t.standbys[], host+port matched, reusing
repl_active_standby_count's ep!=NULL && !in_catch_up predicate) --
dead/catching-up/ok; a non-primary responder reports unknown for every
non-self peer. A self-identity mismatch (pre-existing misconfiguration)
logs one WARN and falls through to the normal non-self sourcing rather
than forcing any peer to ok. Read-only: a bounded lock-protected scan,
no state mutation.

TDD: 3 new tests (primary-sourced-from-registry, standby-unknown-except-
self, self-mismatch-falls-through) plus an extended no-mutation
assertion; RED->GREEN independently reproduced by the reviewer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-16 15:18:15 +00:00
parent 1b278c0e9a
commit ab5dd91537
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 288 additions and 1 deletions

View file

@ -22,6 +22,7 @@
#include "cluster.h"
#include "ha.h"
#include "persist.h"
#include "utils.h" /* WARN */
#include "xce/cluster_def.h"
/*
@ -197,13 +198,89 @@ void ctrlstatus_fill(cluster_t* c, xce_ctrlstatus_t* st)
if (n > 0) {
st->peers.list = (xce_ctrlpeer_t**)calloc((size_t)n,
sizeof(xce_ctrlpeer_t*));
/* per-controller connection status (Req 13.1-13.5; design.md Req 13
* delta, Locked Decisions 3-6): the responder's own entry always
* reports ok; when the responder is currently primary, every OTHER
* peer is sourced by host+port match against the live replication
* connection registry (replicator_t.standbys[]) -- no live
* connection -> dead, live but not yet caught up -> catching-up,
* live and caught up -> ok; when the responder is not primary,
* every other peer is unknown, since only the primary has direct
* connection visibility into every other node. */
int is_primary = (st->responderRole == (uint8_t)CTRL_PRIMARY);
/* ha->repl is a back-pointer wired once commit-engine setup runs
* (cluster.c) and may be NULL before that (or in a minimal test
* fixture); &c->persist.repl is always a valid fallback (mirrors
* the ha->repl ? ha->repl : &p->repl idiom at ha.c:2673). */
replicator_t* repl = ha->repl ? ha->repl : &c->persist.repl;
/* Self-match fallback (design-review finding): if this responder's
* own identity matches none of its configured peers -- a pre-
* existing misconfiguration, not new to this delta -- WARN once per
* call and let every peer fall through to the normal non-self
* branches below. No entry is force-corrected to ok in this case;
* this is an explicit, logged degradation rather than a silent or
* fabricated result. */
int self_found = 0;
for (int i = 0; i < n; i++) {
if (strcmp(mc->peers_cfg.candidates[i].name,
st->responderName) == 0) {
self_found = 1;
break;
}
}
if (!self_found) {
WARN("ctrlstatus: responder identity '%s' matches none of the "
"%d configured peers -- per-peer connection status falls "
"through to the non-self sourcing for every configured "
"peer (check this node's --peers/name configuration)",
st->responderName, n);
}
if (is_primary)
mylock_lock(&repl->lock);
for (int i = 0; i < n; i++) {
xce_ctrlpeer_t* p = (xce_ctrlpeer_t*)calloc(1, sizeof(*p));
p->name = mc->peers_cfg.candidates[i].name;
p->host = mc->peers_cfg.candidates[i].hostname;
p->port = (uint32_t)mc->peers_cfg.candidates[i].port;
if (strcmp(mc->peers_cfg.candidates[i].name,
st->responderName) == 0) {
p->status = 1u; /* self: always ok (Req 13.2) */
} else if (is_primary) {
/* Reuses repl_active_standby_count's exact ep != NULL &&
* !in_catch_up predicate (repl.c) rather than inventing a
* new one (Locked Decision 3). */
uint8_t status = 3u; /* default: no matching live slot */
for (int j = 0; j < MAX_STANDBYS; j++) {
if (strcmp(repl->standbys[j].host,
mc->peers_cfg.candidates[i].hostname) == 0 &&
repl->standbys[j].port == p->port) {
if (repl->standbys[j].ep == NULL) {
status = 3u; /* matched slot, no live conn */
} else if (atomic_load_explicit(
&repl->standbys[j].in_catch_up,
memory_order_acquire)) {
status = 2u; /* catching-up */
} else {
status = 1u; /* ok */
}
break;
}
}
p->status = status;
} else {
p->status = 0u; /* unknown: only the primary can see peers */
}
st->peers.list[i] = p;
}
if (is_primary)
mylock_unlock(&repl->lock);
}
}

View file

@ -302,6 +302,20 @@ static void test_ctrlstatus_no_mutation(void)
atomic_store_explicit(&c.ha.last_known_elected_at_ms,
UINT64_C(1781939830911), memory_order_release);
/* Req 13.5: reading the peer connection-status registry must not mutate
* it either. Populate one live standby slot matching a configured peer
* so the primary-branch registry scan actually executes during this
* call (role is already PRIMARY above). */
TEST_ASSERT_EQUAL_INT(0, repl_init(&c.persist.repl));
c.ha.repl = &c.persist.repl;
int dummy_ep;
c.persist.repl.standbys[0].ep = (repl_ep_t)&dummy_ep;
strncpy(c.persist.repl.standbys[0].host, "10.0.0.2",
sizeof(c.persist.repl.standbys[0].host) - 1);
c.persist.repl.standbys[0].port = 7002;
atomic_store_explicit(&c.persist.repl.standbys[0].in_catch_up, 0,
memory_order_release);
int32_t role0 = atomic_load_explicit(&c.ha.role, memory_order_acquire);
uint64_t term0 = atomic_load_explicit(&c.ha.term, memory_order_acquire);
uint64_t applied0 = atomic_load_explicit(&c.ha.applied_lsn,
@ -317,6 +331,13 @@ static void test_ctrlstatus_no_mutation(void)
uint64_t vertag0 = mc.config_version_tag;
uint64_t elected0 = atomic_load_explicit(&c.ha.last_known_elected_at_ms,
memory_order_acquire);
repl_ep_t ep0 = c.persist.repl.standbys[0].ep;
char host0[XCE_MAXNAME_LEN];
strncpy(host0, c.persist.repl.standbys[0].host, sizeof(host0));
uint32_t port0 = c.persist.repl.standbys[0].port;
int incatchup0 = atomic_load_explicit(
&c.persist.repl.standbys[0].in_catch_up,
memory_order_acquire);
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
@ -343,7 +364,17 @@ static void test_ctrlstatus_no_mutation(void)
TEST_ASSERT_EQUAL_UINT64(elected0,
atomic_load_explicit(&c.ha.last_known_elected_at_ms,
memory_order_acquire));
/* Req 13.5: the connection-registry slot this call's primary-branch
* peer-status scan read must be unchanged too. */
TEST_ASSERT_EQUAL_PTR(ep0, c.persist.repl.standbys[0].ep);
TEST_ASSERT_EQUAL_STRING(host0, c.persist.repl.standbys[0].host);
TEST_ASSERT_EQUAL_UINT32(port0, c.persist.repl.standbys[0].port);
TEST_ASSERT_EQUAL_INT(incatchup0,
atomic_load_explicit(
&c.persist.repl.standbys[0].in_catch_up,
memory_order_acquire));
repl_fini(&c.persist.repl);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_no_mutation: PASS\n");
}
@ -781,6 +812,180 @@ static void test_ctrlstatus_standby_reports_learned_elected_at_ms(void)
printf("test_ctrlstatus_standby_reports_learned_elected_at_ms: PASS\n");
}
/* ── per-controller connection status (task 8.2, Req 13) ──────────────────
* Design delta "Design Delta — Requirement 13", "ctrlstatus_fill / peer-
* status delta — Service contract": self always ok; PRIMARY sources every
* other peer by host+port match against the live replication registry
* (replicator_t.standbys[]); non-primary sources every other peer as
* unknown; a self-match miss (misconfiguration) WARNs once and falls
* through to the normal non-self branches for every peer (no forced ok).
*/
/* (o) PRIMARY: three OTHER configured peers in three distinct connection
* states -- live+caught-up -> ok, live+catching-up -> catching-up, no live
* connection at all -> dead -- plus the responder's own entry, always ok
* regardless of the registry (Req 13.1-13.3, Locked Decisions 3-4). */
static void test_ctrlstatus_peer_status_primary_sourced_from_registry(void)
{
cluster_t c;
myconf_t mc;
cs_setup_cluster(&c, &mc);
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY,
memory_order_release);
/* cs_setup_cluster's default 3 candidates are self=ctrlA + ctrlB/ctrlC;
* add a 4th (MAX_HA_CANDIDATES == 4 covers self + 3 others exactly) with
* no matching registry slot at all. */
mc.peers_cfg.candidates_count = 4;
strncpy(mc.peers_cfg.candidates[3].name, "ctrlD",
sizeof(mc.peers_cfg.candidates[3].name) - 1);
strncpy(mc.peers_cfg.candidates[3].hostname, "10.0.0.4",
sizeof(mc.peers_cfg.candidates[3].hostname) - 1);
mc.peers_cfg.candidates[3].port = 7004;
TEST_ASSERT_EQUAL_INT(0, repl_init(&c.persist.repl));
c.ha.repl = &c.persist.repl;
/* ctrlB (10.0.0.2:7002): live, caught up -> ok */
int dummy_ep_b, dummy_ep_c;
c.persist.repl.standbys[0].ep = (repl_ep_t)&dummy_ep_b;
strncpy(c.persist.repl.standbys[0].host, "10.0.0.2",
sizeof(c.persist.repl.standbys[0].host) - 1);
c.persist.repl.standbys[0].port = 7002;
atomic_store_explicit(&c.persist.repl.standbys[0].in_catch_up, 0,
memory_order_release);
/* ctrlC (10.0.0.3:7003): live, still catching up -> catching-up */
c.persist.repl.standbys[1].ep = (repl_ep_t)&dummy_ep_c;
strncpy(c.persist.repl.standbys[1].host, "10.0.0.3",
sizeof(c.persist.repl.standbys[1].host) - 1);
c.persist.repl.standbys[1].port = 7003;
atomic_store_explicit(&c.persist.repl.standbys[1].in_catch_up, 1,
memory_order_release);
/* standbys[2] left zeroed (ep == NULL, host == "") -> ctrlD (10.0.0.4)
* has no matching slot at all -> dead. */
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
TEST_ASSERT_EQUAL_UINT32(4, st.peerCount);
TEST_ASSERT_EQUAL_INT(4, st.peers.count);
TEST_ASSERT_NOT_NULL(st.peers.list);
TEST_ASSERT_EQUAL_STRING("ctrlA", st.peers.list[0]->name);
TEST_ASSERT_EQUAL_UINT8(1, st.peers.list[0]->status); /* self: ok */
TEST_ASSERT_EQUAL_STRING("ctrlB", st.peers.list[1]->name);
TEST_ASSERT_EQUAL_UINT8(1, st.peers.list[1]->status); /* live+caught up */
TEST_ASSERT_EQUAL_STRING("ctrlC", st.peers.list[2]->name);
TEST_ASSERT_EQUAL_UINT8(2, st.peers.list[2]->status); /* live+catching up */
TEST_ASSERT_EQUAL_STRING("ctrlD", st.peers.list[3]->name);
TEST_ASSERT_EQUAL_UINT8(3, st.peers.list[3]->status); /* no live conn */
ctrlstatus_free_fields(&st);
repl_fini(&c.persist.repl);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_peer_status_primary_sourced_from_registry: PASS\n");
}
/* (p) STANDBY: reports ok for its own entry and unknown (0) for every other
* configured peer, including the primary -- even when a live-looking
* registry entry exists for that peer, since only the primary has direct
* connection visibility into every other node (Req 13.2, 13.4, Locked
* Decision 5). */
static void test_ctrlstatus_peer_status_standby_unknown_except_self(void)
{
cluster_t c;
myconf_t mc;
cs_setup_cluster(&c, &mc);
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
memory_order_release);
/* Even with a live-looking registry entry for ctrlB, a non-primary
* responder must NOT read it -- it has no direct connection visibility
* to any peer but itself. */
TEST_ASSERT_EQUAL_INT(0, repl_init(&c.persist.repl));
c.ha.repl = &c.persist.repl;
int dummy_ep;
c.persist.repl.standbys[0].ep = (repl_ep_t)&dummy_ep;
strncpy(c.persist.repl.standbys[0].host, "10.0.0.2",
sizeof(c.persist.repl.standbys[0].host) - 1);
c.persist.repl.standbys[0].port = 7002;
atomic_store_explicit(&c.persist.repl.standbys[0].in_catch_up, 0,
memory_order_release);
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
TEST_ASSERT_EQUAL_INT(3, st.peers.count);
TEST_ASSERT_EQUAL_STRING("ctrlA", st.peers.list[0]->name);
TEST_ASSERT_EQUAL_UINT8(1, st.peers.list[0]->status); /* self: ok */
TEST_ASSERT_EQUAL_STRING("ctrlB", st.peers.list[1]->name);
TEST_ASSERT_EQUAL_UINT8(0, st.peers.list[1]->status); /* unknown */
TEST_ASSERT_EQUAL_STRING("ctrlC", st.peers.list[2]->name);
TEST_ASSERT_EQUAL_UINT8(0, st.peers.list[2]->status); /* unknown, incl. primary */
ctrlstatus_free_fields(&st);
repl_fini(&c.persist.repl);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_peer_status_standby_unknown_except_self: PASS\n");
}
/* (q) Misconfiguration fallback: a responder whose own identity matches NO
* configured peer still produces a defined, non-crashing result via the
* normal non-self sourcing -- no entry is force-corrected to ok (design.md
* Req 13 delta, self-match fallback). One genuine live+caught-up registry
* slot (matching ctrlB) proves the normal primary-branch sourcing still
* runs for every peer despite the self-match miss; ctrlA -- which would
* have been "self" under correct config -- gets the ordinary unmatched ->
* dead outcome like ctrlC, proving nothing was force-corrected to ok. The
* implementation also WARNs once per call in this case; this codebase has
* no log-content capture harness to assert that in a unit test, so it is
* verified by code inspection instead of a test assertion here. */
static void test_ctrlstatus_peer_status_self_mismatch_falls_through(void)
{
cluster_t c;
myconf_t mc;
cs_setup_cluster(&c, &mc);
/* Own identity matches none of the configured peer names (misconfigured
* --name vs --peers). */
c.myhost_info.name = "ctrlZ";
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY,
memory_order_release);
TEST_ASSERT_EQUAL_INT(0, repl_init(&c.persist.repl));
c.ha.repl = &c.persist.repl;
/* ctrlB (10.0.0.2:7002): live, caught up -> ok (genuine registry match). */
int dummy_ep;
c.persist.repl.standbys[0].ep = (repl_ep_t)&dummy_ep;
strncpy(c.persist.repl.standbys[0].host, "10.0.0.2",
sizeof(c.persist.repl.standbys[0].host) - 1);
c.persist.repl.standbys[0].port = 7002;
atomic_store_explicit(&c.persist.repl.standbys[0].in_catch_up, 0,
memory_order_release);
/* No matching slot for ctrlA (10.0.0.1:7001) or ctrlC (10.0.0.3:7003). */
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
TEST_ASSERT_EQUAL_INT(3, st.peers.count);
TEST_ASSERT_EQUAL_STRING("ctrlA", st.peers.list[0]->name);
TEST_ASSERT_EQUAL_UINT8(3, st.peers.list[0]->status); /* NOT forced to ok */
TEST_ASSERT_EQUAL_STRING("ctrlB", st.peers.list[1]->name);
TEST_ASSERT_EQUAL_UINT8(1, st.peers.list[1]->status); /* genuine match */
TEST_ASSERT_EQUAL_STRING("ctrlC", st.peers.list[2]->name);
TEST_ASSERT_EQUAL_UINT8(3, st.peers.list[2]->status);
ctrlstatus_free_fields(&st);
repl_fini(&c.persist.repl);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_peer_status_self_mismatch_falls_through: PASS\n");
}
/* ── suite entry point ─────────────────────────────────────── */
void test_ctrlstatus(void)
@ -803,4 +1008,7 @@ void test_ctrlstatus(void)
RUN_TEST(test_heartbeatmsg_electedatms_roundtrip);
RUN_TEST(test_ctrlstatus_primary_reports_own_elected_at_ms);
RUN_TEST(test_ctrlstatus_standby_reports_learned_elected_at_ms);
RUN_TEST(test_ctrlstatus_peer_status_primary_sourced_from_registry);
RUN_TEST(test_ctrlstatus_peer_status_standby_unknown_except_self);
RUN_TEST(test_ctrlstatus_peer_status_self_mismatch_falls_through);
}