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
|
|
@ -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