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:
parent
1b278c0e9a
commit
ab5dd91537
3 changed files with 288 additions and 1 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue