From a6e03d87ab46576fe010e9b96dc086064f40b517 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 20 Jun 2026 04:53:27 +0000 Subject: [PATCH] feat(controller-status): handle_ctrlstatus read-only state snapshot + dispatch + configChecksum as Text hex (task 2.1) --- .kiro/specs/controller-status/tasks.md | 3 +- src/controller/cluster.c | 3 + src/controller/ctrlstatus.c | 189 +++++++++++++ src/controller/include/cluster.h | 9 + src/controller/meson.build | 3 + src/controller/testsuite/controller_test.c | 2 + src/controller/testsuite/test_ctrlstatus.c | 292 +++++++++++++++++++++ src/include/typemap.h | 2 +- src/schema/controller.capnp | 2 +- 9 files changed, 502 insertions(+), 3 deletions(-) create mode 100644 src/controller/ctrlstatus.c create mode 100644 src/controller/testsuite/test_ctrlstatus.c diff --git a/.kiro/specs/controller-status/tasks.md b/.kiro/specs/controller-status/tasks.md index 1e5be1c7..c44b875b 100644 --- a/.kiro/specs/controller-status/tasks.md +++ b/.kiro/specs/controller-status/tasks.md @@ -11,7 +11,7 @@ - _Requirements: 1.1, 1.4, 2.1, 3.1, 3.2, 4.1, 4.2, 5.1, 5.2, 6.1, 6.2, 6.3, 6.4_ - [ ] 2. Controller: status handler + any-role dispatch -- [ ] 2.1 Implement the controller status handler +- [x] 2.1 Implement the controller status handler - Add a handler that snapshots the responding node's local HA and config state into the status reply: responder name and role; current primary (taken under the HA lock; empty when none is known/elected); term; loaded config version + content checksum; committed config version; commit and apply positions; fenced state (catch-up or unrecoverable-fence); and the configured controllers list with count. It must be read-only (no state mutation) and answer in every role. - Route the new opcode to this handler in the controller request dispatch switch. - TDD: unit tests assert the field values for a PRIMARY (primary == self, role == primary, populated peers/version/checksum), a STANDBY with a known primary (reports that primary, role == standby), and a node with no known primary (empty primary → "unknown"); and assert the handler mutates no state. @@ -68,3 +68,4 @@ - Task 2.2 is the one HA-critical change (request-handling path): keep it scoped to the single read-only opcode and prove no other opcode's gating widened. - The authoritative redirect (3.2) is what makes wait-primary trustworthy (no stale-standby false success) — it is the resolution of the design-review decision; do not collapse it back into a plain any-node read for wait-primary. - 1.1 LEARNING (affects later tasks): the `$C.mapname(...)` capnp types are NOT auto-generated in this schema — the generated `controller.capnp.c` references HAND-AUTHORED C typedefs in `src/include/typemap.h` (same convention as `xce_clusterconf_t`). So `xce_ctrlstatus_t`/`xce_ctrlpeer_t` (and the `xce_reply_t.data.ctrlstatus` union member) live in `typemap.h`, verified field-for-field against the regenerated codec. `OPCode_ctrlstatus = 40`, `RequestReply_data_ctrlstatus = 12`. `configChecksum` is `xce_data_t*`. The handler/libxce tasks build/encode against these typemap.h types. +- 2.1 LEARNING + SCHEMA AMENDMENT: capnp `Data` in THIS schema maps to the project's STRUCTURED `xce_data_t` union (table-switched), NOT raw bytes — `encode_Data_ptr` can't serialize a SHA-256 blob. So `configChecksum @6` was changed `Data → Text` (a 64-char lowercase-hex string of the full 32-byte `config_sha256`); typemap member is `char*`; freed in `ctrlstatus_free_fields`. Ordinal @6 unchanged, no released decoder yet → safe. Handler lives in NEW `src/controller/ctrlstatus.c` (`ctrlstatus_fill`/`ctrlstatus_free_fields`/`handle_ctrlstatus`), proto in `src/controller/include/cluster.h`, routed at `cluster.c` opcode switch, test `src/controller/testsuite/test_ctrlstatus.c` (registered in `controller_test.c`, built via `controller/meson.build`). `primary_host` read under `ha->lock`; peer name/host strings are BORROWED from myconf (not freed). The libxce client (3.x) decodes `configChecksum` as Text. diff --git a/src/controller/cluster.c b/src/controller/cluster.c index 3487743a..a8e832b5 100644 --- a/src/controller/cluster.c +++ b/src/controller/cluster.c @@ -335,6 +335,9 @@ static int myreq_handler(vtask_t* task) case OPCode_clusterconf: rc = handle_clusterconf(c, ep, &request); break; + case OPCode_ctrlstatus: + rc = handle_ctrlstatus(c, ep, &request); + break; case OPCode_update: rc = do_update_op(c, ep, &request); break; diff --git a/src/controller/ctrlstatus.c b/src/controller/ctrlstatus.c new file mode 100644 index 00000000..7666c51a --- /dev/null +++ b/src/controller/ctrlstatus.c @@ -0,0 +1,189 @@ +/* ctrlstatus.c — controller status query handler (controller-status spec). + * + * Implements the read-only `ctrlstatus` opcode (an XCE analog of LSF `lsid`): + * snapshot the responding node's local HA/config state into a CtrlStatus reply + * and send it. Answerable in every role (the dispatch-gate allow-listing for + * that is task 2.2); this file only builds and sends the reply. + * + * Split into a pure snapshot (ctrlstatus_fill) + a thin send wrapper + * (handle_ctrlstatus), mirroring ha_nop_reply_state_report, so the field + * sourcing is unit-testable without an endpoint/socket. + * + * Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 4.1, 4.2, 5.1, 5.2, 5.3, + * 6.1, 6.2, 6.3, 6.4 (controller-status spec, task 2.1). + */ +#include "config.h" + +#include +#include +#include + +#include "cluster.h" +#include "ha.h" +#include "persist.h" +#include "xce/cluster_def.h" + +/* + * ctrlstatus_fill — snapshot local HA/config state into *st. + * + * Read-only: every field comes from an atomic load, an immutable config read, + * or a lock-guarded copy of primary_host/port. No HA, persist, or config state + * is modified (Req 1.2). + * + * Field sourcing: + * responderName / clusterName — own peerinfo / loaded cluster config + * responderRole / term — ha->role / ha->term atomics (Req 3.2, 6.1) + * primaryName — ha->primary_host under ha->lock; empty + * string when no primary is known (Req 1.3, + * 2.2, 2.3 — client renders empty as "unknown") + * configVersion — loaded config_version_tag (Req 5.1) + * configChecksum — lowercase-hex of myconf->config_sha256[32] + * (64-char Text; Req 5.2 content checksum, + * 5.3 divergence detection) — see NOTE below + * committedConfigVersion — ha->committed_config_tag (Req 5.1) + * committedLsn / appliedLsn — persist->committed_lsn / ha->applied_lsn (6.2) + * fenced — in_catch_up || recovery_unrecoverable (6.3) + * peers / peerCount — myconf->peers_cfg.candidates (Req 4.1, 4.2) + * + * NOTE (configChecksum): the schema field is `configChecksum @6: Text` (it was + * originally `Data`, but `Data` maps to the project's structured `xce_data_t` — + * a tagged union over job/worker/resource/resval table payloads, NOT a capnp + * byte blob, so it cannot carry the raw 32-byte digest). We therefore carry the + * full SHA-256 content checksum as a 64-char lowercase-hex `Text` string, which + * the codec encodes/decodes like any other Text member (clusterName/primaryName) + * and the client renders as hex anyway. This gives Req 5.2 (content checksum) + * and Req 5.3 (same-version divergence detection — identical config bytes on two + * nodes yield the same hex; differing bytes differ). configVersion / + * committedConfigVersion (the 8-byte SHA-256 prefix tag) carry the version-level + * identity for Req 5.1. The hex string is heap-allocated here and freed in + * ctrlstatus_free_fields(). + */ +void ctrlstatus_fill(cluster_t* c, xce_ctrlstatus_t* st) +{ + ha_state_t* ha = &c->ha; + myconf_t* mc = c->myconf; + + memset(st, 0, sizeof(*st)); + + /* responder identity + cluster name */ + st->responderName = (c->myhost && c->myhost->name[0] != '\0') + ? c->myhost->name + : (mc && mc->name) ? mc->name + : ""; + st->clusterName = (mc && mc->cluster_config && mc->cluster_config->clustername) + ? mc->cluster_config->clustername + : ""; + + /* role / term (atomics) */ + st->responderRole = (uint8_t)atomic_load_explicit(&ha->role, + memory_order_acquire); + st->term = atomic_load_explicit(&ha->term, memory_order_acquire); + + /* primary identity — guarded by ha->lock (Req 2.2, 2.3) */ + pthread_mutex_lock(&ha->lock); + st->primaryName = (ha->primary_host[0] != '\0') ? strdup(ha->primary_host) + : strdup(""); + pthread_mutex_unlock(&ha->lock); + + /* loaded config version (+ committed version) */ + st->configVersion = mc ? mc->config_version_tag : 0; + st->committedConfigVersion = atomic_load_explicit(&ha->committed_config_tag, + memory_order_acquire); + + /* configChecksum: 64-char lowercase-hex of the loaded config_sha256[32] + * (Req 5.2 content checksum, 5.3 divergence detection — see NOTE above). + * Heap-allocated; freed in ctrlstatus_free_fields(). */ + st->configChecksum = NULL; + if (mc) { + static const char hexdig[] = "0123456789abcdef"; + char* hex = (char*)malloc(2u * sizeof(mc->config_sha256) + 1u); + if (hex) { + for (size_t i = 0; i < sizeof(mc->config_sha256); i++) { + hex[2 * i] = hexdig[(mc->config_sha256[i] >> 4) & 0x0F]; + hex[2 * i + 1] = hexdig[mc->config_sha256[i] & 0x0F]; + } + hex[2 * sizeof(mc->config_sha256)] = '\0'; + st->configChecksum = hex; + } + } + + /* commit / apply progress (Req 6.2) */ + st->committedLsn = atomic_load_explicit(&c->persist.committed_lsn, + memory_order_acquire); + st->appliedLsn = atomic_load_explicit(&ha->applied_lsn, + memory_order_acquire); + + /* fenced: catch-up OR recovery-unrecoverable (Req 6.3) */ + int in_catch_up = atomic_load_explicit(&ha->in_catch_up, + memory_order_acquire); + st->fenced = (in_catch_up || persist_recovery_unrecoverable(&c->persist)) + ? 1u + : 0u; + + /* configured controllers (Req 4.1, 4.2) */ + int n = 0; + if (mc) { + n = mc->peers_cfg.candidates_count; + if (n < 0) + n = 0; + } + st->peerCount = (uint32_t)n; + st->peers.count = n; + st->peers.list = NULL; + if (n > 0) { + st->peers.list = (xce_ctrlpeer_t**)calloc((size_t)n, + sizeof(xce_ctrlpeer_t*)); + 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; + st->peers.list[i] = p; + } + } +} + +/* + * ctrlstatus_free_fields — release the heap that ctrlstatus_fill allocated. + * Frees the per-peer structs + the pointer array and the primaryName copy. + * The peer name/host strings are NOT owned (they point into myconf) and are + * not freed. Safe to call once after the reply has been serialised. + */ +void ctrlstatus_free_fields(xce_ctrlstatus_t* st) +{ + if (st->peers.list) { + for (int i = 0; i < st->peers.count; i++) + free(st->peers.list[i]); + free(st->peers.list); + st->peers.list = NULL; + st->peers.count = 0; + } + free(st->primaryName); + st->primaryName = NULL; + free(st->configChecksum); + st->configChecksum = NULL; +} + +/* + * handle_ctrlstatus — dispatch entry point for OPCode_ctrlstatus. + * Builds a CtrlStatus from local state, replies, then frees. Mirrors + * handle_clusterconf (reply.c): stack reply, init_reply_by_req, point the union + * at the payload, xce_reply, then free what was allocated. + */ +int handle_ctrlstatus(cluster_t* c, endpoint_t* ep, xce_request_t* request) +{ + xce_reply_t reply; + xce_ctrlstatus_t status; + + ctrlstatus_fill(c, &status); + + init_reply_by_req(&reply, request); + reply.status = 0; + reply.data_which = RequestReply_data_ctrlstatus; + reply.data.ctrlstatus = &status; + + xce_reply(ep, c->myhost, &reply); + + ctrlstatus_free_fields(&status); + return 0; +} diff --git a/src/controller/include/cluster.h b/src/controller/include/cluster.h index 54f1704e..fc881d5a 100644 --- a/src/controller/include/cluster.h +++ b/src/controller/include/cluster.h @@ -79,6 +79,15 @@ extern int mark_worker_back(cluster_t*, worker_t*); extern int handle_scheduler_leave(cluster_t*); extern int handle_clusterconf(cluster_t*, endpoint_t*, xce_request_t*); +/* controller-status spec (task 2.1): read-only status query. + * ctrlstatus_fill snapshots local HA/config state into *st (no I/O, no state + * mutation); ctrlstatus_free_fields releases what fill allocated (the peer + * pointer array). handle_ctrlstatus is the dispatch entry point that fills, + * replies via xce_reply, then frees. */ +extern void ctrlstatus_fill(cluster_t*, xce_ctrlstatus_t*); +extern void ctrlstatus_free_fields(xce_ctrlstatus_t*); +extern int handle_ctrlstatus(cluster_t*, endpoint_t*, xce_request_t*); + extern void collect_confvars(cluster_t*, collect_check_fn_t, xlist_t*); extern void collect_queues(cluster_t*, collect_check_fn_t, xlist_t*); extern void collect_resources(cluster_t*, collect_check_fn_t, xlist_t*); diff --git a/src/controller/meson.build b/src/controller/meson.build index d5f43058..35261bbb 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -2,6 +2,7 @@ controller_src = [ 'cluster.c', 'commit.c', 'config_repl.c', + 'ctrlstatus.c', 'dataex.c', 'egress.c', 'ha.c', @@ -132,6 +133,8 @@ controller_unit_test_src = [ 'testsuite/test_convergence_verification.c', 'testsuite/test_coldstart_lsn0.c', 'testsuite/test_commit_durability.c', + 'testsuite/test_ctrlstatus.c', + 'ctrlstatus.c', 'commit.c', 'job.c', 'egress.c', diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index 4f6bceab..2f48718b 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -60,6 +60,7 @@ extern void test_demotion_convergence(void); extern void test_convergence_verification(void); extern void test_coldstart_lsn0(void); extern void test_commit_durability(void); +extern void test_ctrlstatus(void); void setUp(void) { return; } @@ -189,6 +190,7 @@ int main(int argc, char* argv[]) TEST(test_convergence_verification); TEST(test_coldstart_lsn0); TEST(test_commit_durability); + TEST(test_ctrlstatus); return UNITY_END(); } diff --git a/src/controller/testsuite/test_ctrlstatus.c b/src/controller/testsuite/test_ctrlstatus.c new file mode 100644 index 00000000..011ac4fa --- /dev/null +++ b/src/controller/testsuite/test_ctrlstatus.c @@ -0,0 +1,292 @@ +/* test_ctrlstatus.c — unit tests for the controller status handler + * (controller-status spec, task 2.1). + * + * Exercises ctrlstatus_fill(), the pure local-state snapshot used by + * handle_ctrlstatus(). Splitting the snapshot from the socket send (mirroring + * ha_nop_reply_state_report) makes the field sourcing unit-testable without an + * endpoint. Covers Requirements 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 4.1, 4.2, + * 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4. + */ +#include +#include +#include + +#include + +#include "ha.h" +#include "include/cluster.h" +#include "myconf.h" +#include "persist.h" +#include "xce/cluster_def.h" + +/* ── helpers ────────────────────────────────────────────────── */ + +static ha_config_t cs_valid_cfg(void) +{ + ha_config_t cfg = { + .heartbeat_interval_ms = 3000, + .heartbeat_timeout_ms = 10000, + .election_timeout_ms = 2000, + .probe_connect_timeout_ms = 500, + .max_promotion_retries = 5, + }; + return cfg; +} + +/* Build a minimal cluster_t whose ha/myconf fields the handler reads. + * Only the fields ctrlstatus_fill() touches are initialised. */ +static void cs_setup_cluster(cluster_t* c, myconf_t* mc) +{ + memset(c, 0, sizeof(*c)); + memset(mc, 0, sizeof(*mc)); + + ha_config_t cfg = cs_valid_cfg(); + TEST_ASSERT_EQUAL_INT(0, ha_init(&c->ha, &cfg, 0)); + + c->myconf = mc; + mc->name = "ctrlA"; + mc->cluster_config = NULL; /* cluster name read from peers/own identity */ + + /* own identity (xce_peerinfo_t.name is a char*, not a buffer) */ + c->myhost_info.name = "ctrlA"; + c->myhost = &c->myhost_info; + + /* loaded config version + checksum */ + mc->config_version_tag = 0xAABBCCDDEE001122ULL; + for (int i = 0; i < 32; i++) + mc->config_sha256[i] = (uint8_t)(i + 1); + + /* configured controllers (peers) */ + mc->peers_cfg.candidates_count = 3; + strncpy(mc->peers_cfg.candidates[0].name, "ctrlA", + sizeof(mc->peers_cfg.candidates[0].name) - 1); + strncpy(mc->peers_cfg.candidates[0].hostname, "10.0.0.1", + sizeof(mc->peers_cfg.candidates[0].hostname) - 1); + mc->peers_cfg.candidates[0].port = 7001; + strncpy(mc->peers_cfg.candidates[1].name, "ctrlB", + sizeof(mc->peers_cfg.candidates[1].name) - 1); + strncpy(mc->peers_cfg.candidates[1].hostname, "10.0.0.2", + sizeof(mc->peers_cfg.candidates[1].hostname) - 1); + mc->peers_cfg.candidates[1].port = 7002; + strncpy(mc->peers_cfg.candidates[2].name, "ctrlC", + sizeof(mc->peers_cfg.candidates[2].name) - 1); + strncpy(mc->peers_cfg.candidates[2].hostname, "10.0.0.3", + sizeof(mc->peers_cfg.candidates[2].hostname) - 1); + mc->peers_cfg.candidates[2].port = 7003; +} + +static void cs_teardown_cluster(cluster_t* c) +{ + ha_fini(&c->ha); +} + +/* ── tests ──────────────────────────────────────────────────── */ + +/* (a) PRIMARY: responderRole==PRIMARY, primaryName==self, peers/version + * populated; committed/applied/term reflect the atomics. (Req 2.1, 3.1, 3.2, + * 4.1, 4.2, 5.1, 6.1, 6.2, 6.4) */ +static void test_ctrlstatus_primary(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); + atomic_store_explicit(&c.ha.term, 7, memory_order_release); + atomic_store_explicit(&c.ha.applied_lsn, 42, memory_order_release); + atomic_store_explicit(&c.ha.committed_config_tag, 0xAABBCCDDEE001122ULL, + memory_order_release); + atomic_store_explicit(&c.persist.committed_lsn, 41, memory_order_release); + /* A primary knows itself as the primary */ + strncpy(c.ha.primary_host, "ctrlA", sizeof(c.ha.primary_host) - 1); + c.ha.primary_port = 7001; + + xce_ctrlstatus_t st; + ctrlstatus_fill(&c, &st); + + TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_PRIMARY, st.responderRole); + TEST_ASSERT_NOT_NULL(st.responderName); + TEST_ASSERT_EQUAL_STRING("ctrlA", st.responderName); + TEST_ASSERT_NOT_NULL(st.primaryName); + TEST_ASSERT_EQUAL_STRING("ctrlA", st.primaryName); + TEST_ASSERT_EQUAL_UINT64(7, st.term); + TEST_ASSERT_EQUAL_UINT64(42, st.appliedLsn); + TEST_ASSERT_EQUAL_UINT64(41, st.committedLsn); + TEST_ASSERT_EQUAL_UINT64(0xAABBCCDDEE001122ULL, st.configVersion); + TEST_ASSERT_EQUAL_UINT64(0xAABBCCDDEE001122ULL, st.committedConfigVersion); + /* configChecksum: 64-char lowercase-hex of config_sha256[i]=(i+1), i.e. + * bytes 0x01..0x20. (Req 5.2 content checksum, 5.3 divergence detection) */ + TEST_ASSERT_NOT_NULL(st.configChecksum); + TEST_ASSERT_EQUAL_UINT(64u, (unsigned)strlen(st.configChecksum)); + TEST_ASSERT_EQUAL_STRING( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + st.configChecksum); + TEST_ASSERT_EQUAL_UINT32(3, st.peerCount); + TEST_ASSERT_EQUAL_INT(3, st.peers.count); + TEST_ASSERT_NOT_NULL(st.peers.list); + TEST_ASSERT_EQUAL_STRING("ctrlB", st.peers.list[1]->name); + TEST_ASSERT_EQUAL_STRING("10.0.0.2", st.peers.list[1]->host); + TEST_ASSERT_EQUAL_UINT32(7002, st.peers.list[1]->port); + TEST_ASSERT_EQUAL_UINT(0, st.fenced); + + ctrlstatus_free_fields(&st); + cs_teardown_cluster(&c); + printf("test_ctrlstatus_primary: PASS\n"); +} + +/* (b) STANDBY with known primary: reports that primary, role==STANDBY. + * (Req 2.2, 3.1, 3.2) */ +static void test_ctrlstatus_standby_known_primary(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); + strncpy(c.ha.primary_host, "ctrlB", sizeof(c.ha.primary_host) - 1); + c.ha.primary_port = 7002; + + /* All-0xFF digest → 64 'f' chars: a distinct content checksum from the + * primary case proves Req 5.3 — same version tag but differing config bytes + * produce a differing hex; identical bytes would yield identical hex. */ + for (int i = 0; i < 32; i++) + mc.config_sha256[i] = 0xFFu; + + xce_ctrlstatus_t st; + ctrlstatus_fill(&c, &st); + + TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole); + TEST_ASSERT_EQUAL_STRING("ctrlA", st.responderName); + TEST_ASSERT_NOT_NULL(st.primaryName); + TEST_ASSERT_EQUAL_STRING("ctrlB", st.primaryName); + TEST_ASSERT_NOT_NULL(st.configChecksum); + TEST_ASSERT_EQUAL_UINT(64u, (unsigned)strlen(st.configChecksum)); + TEST_ASSERT_EQUAL_STRING( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + st.configChecksum); + + ctrlstatus_free_fields(&st); + cs_teardown_cluster(&c); + printf("test_ctrlstatus_standby_known_primary: PASS\n"); +} + +/* (c) No known primary → empty primaryName (client renders "unknown"). + * (Req 1.3, 2.3) */ +static void test_ctrlstatus_unknown_primary(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); + /* primary_host left empty by ha_init → unknown */ + + xce_ctrlstatus_t st; + ctrlstatus_fill(&c, &st); + + TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole); + TEST_ASSERT_NOT_NULL(st.primaryName); + TEST_ASSERT_EQUAL_STRING("", st.primaryName); /* empty ⇒ "unknown" */ + + ctrlstatus_free_fields(&st); + cs_teardown_cluster(&c); + printf("test_ctrlstatus_unknown_primary: PASS\n"); +} + +/* (d) fenced reflects in_catch_up. (Req 6.3) */ +static void test_ctrlstatus_fenced_when_in_catch_up(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); + atomic_store_explicit(&c.ha.in_catch_up, 1, memory_order_release); + + xce_ctrlstatus_t st; + ctrlstatus_fill(&c, &st); + + TEST_ASSERT_EQUAL_UINT(1u, st.fenced ? 1u : 0u); + + ctrlstatus_free_fields(&st); + cs_teardown_cluster(&c); + printf("test_ctrlstatus_fenced_when_in_catch_up: PASS\n"); +} + +/* (e) No-mutation: ctrlstatus_fill() must not change any HA/config state. + * Snapshot the relevant fields, call fill, assert they are unchanged. (Req 1.2) + */ +static void test_ctrlstatus_no_mutation(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); + atomic_store_explicit(&c.ha.term, 9, memory_order_release); + atomic_store_explicit(&c.ha.applied_lsn, 100, memory_order_release); + atomic_store_explicit(&c.persist.committed_lsn, 99, memory_order_release); + atomic_store_explicit(&c.ha.committed_config_tag, 0x1234ULL, + memory_order_release); + atomic_store_explicit(&c.ha.in_catch_up, 0, memory_order_release); + strncpy(c.ha.primary_host, "ctrlA", sizeof(c.ha.primary_host) - 1); + c.ha.primary_port = 7001; + + 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, + memory_order_acquire); + uint64_t committed0 = atomic_load_explicit(&c.persist.committed_lsn, + memory_order_acquire); + uint64_t cfgtag0 = atomic_load_explicit(&c.ha.committed_config_tag, + memory_order_acquire); + int catchup0 = atomic_load_explicit(&c.ha.in_catch_up, + memory_order_acquire); + char primary0[XCE_MAXNAME_LEN]; + strncpy(primary0, c.ha.primary_host, sizeof(primary0)); + uint32_t port0 = c.ha.primary_port; + uint64_t vertag0 = mc.config_version_tag; + + xce_ctrlstatus_t st; + ctrlstatus_fill(&c, &st); + ctrlstatus_free_fields(&st); + + TEST_ASSERT_EQUAL_INT(role0, atomic_load_explicit(&c.ha.role, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(term0, atomic_load_explicit(&c.ha.term, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(applied0, + atomic_load_explicit(&c.ha.applied_lsn, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(committed0, + atomic_load_explicit(&c.persist.committed_lsn, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(cfgtag0, + atomic_load_explicit(&c.ha.committed_config_tag, + memory_order_acquire)); + TEST_ASSERT_EQUAL_INT(catchup0, + atomic_load_explicit(&c.ha.in_catch_up, + memory_order_acquire)); + TEST_ASSERT_EQUAL_STRING(primary0, c.ha.primary_host); + TEST_ASSERT_EQUAL_UINT32(port0, c.ha.primary_port); + TEST_ASSERT_EQUAL_UINT64(vertag0, mc.config_version_tag); + + cs_teardown_cluster(&c); + printf("test_ctrlstatus_no_mutation: PASS\n"); +} + +/* ── suite entry point ─────────────────────────────────────── */ + +void test_ctrlstatus(void) +{ + RUN_TEST(test_ctrlstatus_primary); + RUN_TEST(test_ctrlstatus_standby_known_primary); + RUN_TEST(test_ctrlstatus_unknown_primary); + RUN_TEST(test_ctrlstatus_fenced_when_in_catch_up); + RUN_TEST(test_ctrlstatus_no_mutation); +} diff --git a/src/include/typemap.h b/src/include/typemap.h index ae49edd2..54955802 100644 --- a/src/include/typemap.h +++ b/src/include/typemap.h @@ -220,7 +220,7 @@ typedef struct { uint8_t responderRole; uint64_t term; uint64_t configVersion; - xce_data_t* configChecksum; + char* configChecksum; uint64_t committedConfigVersion; uint64_t committedLsn; uint64_t appliedLsn; diff --git a/src/schema/controller.capnp b/src/schema/controller.capnp index 7bb3f56b..4fa99ab4 100644 --- a/src/schema/controller.capnp +++ b/src/schema/controller.capnp @@ -500,7 +500,7 @@ struct CtrlStatus $C.mapname("xce_ctrlstatus_t") { responderRole @3: UInt8; term @4: UInt64; configVersion @5: UInt64; - configChecksum @6: Data; + configChecksum @6: Text; committedConfigVersion @7: UInt64; committedLsn @8: UInt64; appliedLsn @9: UInt64;