feat(controller-status): handle_ctrlstatus read-only state snapshot + dispatch + configChecksum as Text hex (task 2.1)

This commit is contained in:
Rongsong Shen 2026-06-20 04:53:27 +00:00
parent 97a501e96e
commit a6e03d87ab
9 changed files with 502 additions and 3 deletions

View file

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

View file

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

189
src/controller/ctrlstatus.c Normal file
View file

@ -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 <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View file

@ -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*);

View file

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

View file

@ -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();
}

View file

@ -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 <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
#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);
}

View file

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

View file

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