feat(controller-status): libxce xcecli_ctrlstatus any-node read + decode + free (task 3.1)
This commit is contained in:
parent
e56409cbd4
commit
691cd9199e
12 changed files with 285 additions and 1 deletions
|
|
@ -27,7 +27,7 @@
|
|||
- _Depends: 2.1_
|
||||
|
||||
- [ ] 3. libxce client API
|
||||
- [ ] 3.1 Add the failover-aware any-node status call
|
||||
- [x] 3.1 Add the failover-aware any-node status call
|
||||
- Add the opcode-only request sender, the send-and-decode call, and the failover-aware client read (retryable, non-write) that returns whatever reachable node answers, plus the free helper. On no reachable controller, return a typed failure without a partial/empty success.
|
||||
- TDD: a round-trip test decodes every status field from a handler reply; a no-reachable-controller test returns a typed failure.
|
||||
- Observable: the client decodes all fields from a live/loopback handler reply and surfaces a typed error when no controller is reachable.
|
||||
|
|
@ -69,3 +69,4 @@
|
|||
- 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.
|
||||
- 3.1 LEARNING: the ctx-side reply union `xce_ctx_t.reply.u` (in `src/include/xcectx.h`) is SEPARATE from `xce_reply_t.data` (typemap.h) — the decode path needs BOTH a `ctrlstatus` member there AND a `case OPCode_ctrlstatus` in `xcectx.c` handle_reply (mirror clusterconf). Free: clusterconf's `xce_free_clusterconf` macro is a LATENT-BROKEN alias (undefined symbol, never expanded); use the REAL generated free — `xce_free_ctrlstatus(s) → free_CtrlStatus(s)`, `xcecli_free_ctrlstatus(s) → xce_free_ctrlstatus(s)` (any TU expanding it must include the generated `controller.capnp.h`). `xcecli_ctrlstatus` is is_write=false; NULL ctx → `-EINVAL`.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ extern int xcecli_workerlist(xcecli_ctx_t, xce_worker_t***);
|
|||
extern int xcecli_jobdispatch(xcecli_ctx_t, xce_job_dispatch_t*);
|
||||
extern int xcecli_joblist(xcecli_ctx_t, xce_job_t***);
|
||||
extern int xcecli_clusterconf(xcecli_ctx_t, xce_clusterconf_t**);
|
||||
extern int xcecli_ctrlstatus(xcecli_ctx_t, xce_ctrlstatus_t**);
|
||||
extern int xcecli_notify_jobfinish(xcecli_ctx_t, xce_jobfinish_t*);
|
||||
extern int xcecli_notify_jobstart(xcecli_ctx_t, job_pidinfo_t*);
|
||||
extern int xcecli_notify_jobfail(xcecli_ctx_t c, job_failinfo_t*);
|
||||
|
|
@ -88,5 +89,9 @@ extern int xcecli_jobinfo(xcecli_ctx_t, uint64_t, xce_job_t**);
|
|||
#define xcecli_free_queues(nqueues, queues) xce_free_queues(nqueues, queues)
|
||||
#define xcecli_free_groups(ngroups, groups) xce_free_groups(ngroups, groups)
|
||||
#define xcecli_free_clusterconf(conf) xce_free_clusterconf(conf)
|
||||
/* ctrlstatus reply is freed by the generated codec free (free_CtrlStatus,
|
||||
* declared in the generated controller.capnp.h). */
|
||||
#define xce_free_ctrlstatus(s) free_CtrlStatus(s)
|
||||
#define xcecli_free_ctrlstatus(s) xce_free_ctrlstatus(s)
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ extern int xce_startjob_req(endpoint_t*, xce_job_exec_t*);
|
|||
extern int xce_clusterconf_req(endpoint_t*);
|
||||
extern int xce_reply_clusterconf(endpoint_t*, uint64_t, int, xce_peerinfo_t*,
|
||||
xce_clusterconf_t*);
|
||||
extern int xce_ctrlstatus_req(endpoint_t*);
|
||||
extern int xce_get_req(endpoint_t*, xce_get_t*);
|
||||
extern int xce_reply_get(endpoint_t*, xce_peerinfo_t*, xce_reply_t*,
|
||||
xce_check_fn_t, void*, int);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ extern int xce_jobdispatch(xce_ctx_t*, xce_job_dispatch_t*);
|
|||
extern int xce_jobsubmit(xce_ctx_t*, xce_job_submit_t*, uint64_t*);
|
||||
extern int xce_joblist(xce_ctx_t*, xce_job_t***);
|
||||
extern int xce_clusterconf(xce_ctx_t*, xce_clusterconf_t**);
|
||||
extern int xce_ctrlstatus(xce_ctx_t*, xce_ctrlstatus_t**);
|
||||
extern int xce_push(xce_ctx_t*, xce_check_fn_t, void*, int, int, int, void**);
|
||||
extern int xce_update_rows(xce_ctx_t*, int, int, xlist_t*);
|
||||
extern int xce_resval(xce_ctx_t*, char*, char*, xce_resval_t***);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ typedef struct {
|
|||
dataset_t* get;
|
||||
uint64_t jobid;
|
||||
xce_clusterconf_t* clusterconf;
|
||||
xce_ctrlstatus_t* ctrlstatus;
|
||||
} u;
|
||||
} reply;
|
||||
} xce_ctx_t;
|
||||
|
|
|
|||
|
|
@ -324,6 +324,18 @@ int xcecli_clusterconf(xcecli_ctx_t c, xce_clusterconf_t** conf)
|
|||
xce_clusterconf(&(ctx->ctx), conf));
|
||||
}
|
||||
|
||||
int xcecli_ctrlstatus(xcecli_ctx_t c, xce_ctrlstatus_t** status)
|
||||
{
|
||||
api_ctx_t* ctx = (api_ctx_t*)c;
|
||||
|
||||
if (c == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return XCECLI_FAILOVER_CALL(ctx, /*is_write=*/false,
|
||||
xce_ctrlstatus(&(ctx->ctx), status));
|
||||
}
|
||||
|
||||
int xcecli_notify_jobfinish(xcecli_ctx_t c, xce_jobfinish_t* jfinish)
|
||||
{
|
||||
api_ctx_t* ctx = (api_ctx_t*)c;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ libxce_test_src = ['testsuite/test.c',
|
|||
'testsuite/test_request_send_propagation.c',
|
||||
'testsuite/test_raft.c',
|
||||
'testsuite/test_wal_wire.c',
|
||||
'testsuite/test_ctrlstatus.c',
|
||||
'raft/raft_sim.c',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1743,6 +1743,18 @@ int xce_clusterconf_req(endpoint_t* ep)
|
|||
return xce_request(ep, &request);
|
||||
}
|
||||
|
||||
int xce_ctrlstatus_req(endpoint_t* ep)
|
||||
{
|
||||
/* clang-format off */
|
||||
xce_request_t request = {
|
||||
.opcode = OPCode_ctrlstatus,
|
||||
.body_which = Request_body_nomsg
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
return xce_request(ep, &request);
|
||||
}
|
||||
|
||||
int xce_update_req(endpoint_t* ep, xce_update_data_t* data)
|
||||
{
|
||||
/* clang-format off */
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ extern void test_netio_close_propagation(void);
|
|||
extern void test_request_send_propagation(void);
|
||||
extern void test_raft(void);
|
||||
extern void test_wal_wire(void);
|
||||
extern void test_ctrlstatus(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -151,6 +152,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_request_send_propagation);
|
||||
TEST(test_raft);
|
||||
TEST(test_wal_wire);
|
||||
TEST(test_ctrlstatus);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
211
src/lib/testsuite/test_ctrlstatus.c
Normal file
211
src/lib/testsuite/test_ctrlstatus.c
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/* test_ctrlstatus.c — Unity tests for the controller-status (ctrlstatus)
|
||||
* libxce client reply path (controller-status task 3.1).
|
||||
*
|
||||
* Proves the reply path that xcecli_ctrlstatus relies on:
|
||||
* 1. A true ENCODE→DECODE round-trip of xce_ctrlstatus_t through the
|
||||
* generated Cap'n Proto codec (encode_CtrlStatus/write_CtrlStatus →
|
||||
* bytes → read_CtrlStatus/decode_CtrlStatus): every status field,
|
||||
* including the peers list and the configChecksum hex Text, survives
|
||||
* the wire (Req 1.1 — the client decodes every field a handler emits).
|
||||
* 2. xcecli_ctrlstatus(NULL, ...) returns a typed negative rather than a
|
||||
* partial/empty success (Req 7.3 — no reachable controller ⇒ typed
|
||||
* failure).
|
||||
*
|
||||
* A full live round-trip (client → running controller → handler reply)
|
||||
* needs a controller and is the E2E task; here we prove the encode/decode
|
||||
* round-trip + the no-context typed failure only.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "client.h"
|
||||
#include "request.h"
|
||||
#include "xce/types.h"
|
||||
|
||||
/* generated Cap'n Proto codec for CtrlStatus / CtrlPeer */
|
||||
#include "controller.capnp.h"
|
||||
|
||||
/* ── encode→decode round-trip of xce_ctrlstatus_t ───────────────────────── */
|
||||
|
||||
static void test_ctrlstatus_roundtrip_all_fields(void)
|
||||
{
|
||||
/* Arrange: a fully-populated status with two configured peers */
|
||||
xce_ctrlpeer_t p0 = {
|
||||
.name = "ctrl-a", .host = "10.0.0.1", .port = 9991
|
||||
};
|
||||
xce_ctrlpeer_t p1 = {
|
||||
.name = "ctrl-b", .host = "10.0.0.2", .port = 9992
|
||||
};
|
||||
xce_ctrlpeer_t* peerlist[2] = { &p0, &p1 };
|
||||
|
||||
xce_ctrlstatus_t in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.clusterName = "lab-cluster";
|
||||
in.primaryName = "ctrl-a";
|
||||
in.responderName = "ctrl-b";
|
||||
in.responderRole = 3; /* STANDBY */
|
||||
in.term = 42;
|
||||
in.configVersion = 0x1122334455667788ULL;
|
||||
in.configChecksum = "1122334455667788aabbccddeeff0011"
|
||||
"2233445566778899aabbccddeeff0011";
|
||||
in.committedConfigVersion = 0x1122334455667788ULL;
|
||||
in.committedLsn = 5000;
|
||||
in.appliedLsn = 4999;
|
||||
in.fenced = 1;
|
||||
in.peers.count = 2;
|
||||
in.peers.list = peerlist;
|
||||
in.peerCount = 2;
|
||||
|
||||
/* Act: encode into a capnp segment, serialize to bytes, then decode
|
||||
* a fresh struct back out — mirrors xce_encode/xce_decode. */
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
struct CtrlStatus cap_in;
|
||||
CtrlStatus_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
|
||||
memset(&cap_in, 0, sizeof(cap_in));
|
||||
encode_CtrlStatus(cs, &cap_in, &in);
|
||||
|
||||
sp = new_CtrlStatus(cs);
|
||||
write_CtrlStatus(&cap_in, sp);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
size_t sz = capn_size(&c);
|
||||
uint8_t* buf = (uint8_t*)malloc(sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
TEST_ASSERT_EQUAL_size_t(sz, capn_write_mem(&c, buf, sz, 0));
|
||||
capn_free(&c);
|
||||
|
||||
/* decode side */
|
||||
struct capn dc;
|
||||
struct CtrlStatus cap_out;
|
||||
CtrlStatus_ptr dp;
|
||||
xce_ctrlstatus_t out;
|
||||
|
||||
capn_init_mem(&dc, buf, sz, 0);
|
||||
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
||||
read_CtrlStatus(&cap_out, dp);
|
||||
memset(&out, 0, sizeof(out));
|
||||
decode_CtrlStatus(&out, &cap_out);
|
||||
|
||||
/* Assert: every scalar/text field survived */
|
||||
TEST_ASSERT_NOT_NULL(out.clusterName);
|
||||
TEST_ASSERT_EQUAL_STRING("lab-cluster", out.clusterName);
|
||||
TEST_ASSERT_NOT_NULL(out.primaryName);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.primaryName);
|
||||
TEST_ASSERT_NOT_NULL(out.responderName);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-b", out.responderName);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, out.responderRole);
|
||||
TEST_ASSERT_EQUAL_UINT64(42, out.term);
|
||||
TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.configVersion);
|
||||
TEST_ASSERT_NOT_NULL(out.configChecksum);
|
||||
TEST_ASSERT_EQUAL_STRING(in.configChecksum, out.configChecksum);
|
||||
TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.committedConfigVersion);
|
||||
TEST_ASSERT_EQUAL_UINT64(5000, out.committedLsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(4999, out.appliedLsn);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, out.fenced);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, out.peerCount);
|
||||
|
||||
/* Assert: the peers list survived with both entries */
|
||||
TEST_ASSERT_EQUAL_INT(2, out.peers.count);
|
||||
TEST_ASSERT_NOT_NULL(out.peers.list);
|
||||
TEST_ASSERT_NOT_NULL(out.peers.list[0]);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.peers.list[0]->name);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.0.1", out.peers.list[0]->host);
|
||||
TEST_ASSERT_EQUAL_UINT32(9991, out.peers.list[0]->port);
|
||||
TEST_ASSERT_NOT_NULL(out.peers.list[1]);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-b", out.peers.list[1]->name);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.0.2", out.peers.list[1]->host);
|
||||
TEST_ASSERT_EQUAL_UINT32(9992, out.peers.list[1]->port);
|
||||
|
||||
free_CtrlStatus(&out);
|
||||
capn_free(&dc);
|
||||
free(buf);
|
||||
|
||||
printf("test_ctrlstatus_roundtrip_all_fields: PASS\n");
|
||||
}
|
||||
|
||||
/* An "unknown" primary (empty primaryName) must round-trip as empty/NULL,
|
||||
* never garbage — the client renders it as "unknown" (Req 1.3, 2.2). */
|
||||
static void test_ctrlstatus_roundtrip_unknown_primary(void)
|
||||
{
|
||||
xce_ctrlstatus_t in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.clusterName = "lab-cluster";
|
||||
in.primaryName = ""; /* unknown */
|
||||
in.responderName = "ctrl-a";
|
||||
in.responderRole = 1; /* ELECTING */
|
||||
in.peers.count = 0;
|
||||
in.peers.list = NULL;
|
||||
in.peerCount = 0;
|
||||
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
struct CtrlStatus cap_in;
|
||||
CtrlStatus_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
memset(&cap_in, 0, sizeof(cap_in));
|
||||
encode_CtrlStatus(cs, &cap_in, &in);
|
||||
sp = new_CtrlStatus(cs);
|
||||
write_CtrlStatus(&cap_in, sp);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
size_t sz = capn_size(&c);
|
||||
uint8_t* buf = (uint8_t*)malloc(sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
capn_write_mem(&c, buf, sz, 0);
|
||||
capn_free(&c);
|
||||
|
||||
struct capn dc;
|
||||
struct CtrlStatus cap_out;
|
||||
CtrlStatus_ptr dp;
|
||||
xce_ctrlstatus_t out;
|
||||
|
||||
capn_init_mem(&dc, buf, sz, 0);
|
||||
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
||||
read_CtrlStatus(&cap_out, dp);
|
||||
memset(&out, 0, sizeof(out));
|
||||
decode_CtrlStatus(&out, &cap_out);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.responderName);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, out.responderRole);
|
||||
/* empty primaryName decodes to NULL or "" — not a stale/guessed value */
|
||||
if (out.primaryName != NULL) {
|
||||
TEST_ASSERT_EQUAL_STRING("", out.primaryName);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(0, out.peers.count);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, out.peerCount);
|
||||
|
||||
free_CtrlStatus(&out);
|
||||
capn_free(&dc);
|
||||
free(buf);
|
||||
|
||||
printf("test_ctrlstatus_roundtrip_unknown_primary: PASS\n");
|
||||
}
|
||||
|
||||
/* ── no reachable controller ⇒ typed failure (Req 7.3) ──────────────────── */
|
||||
|
||||
static void test_xcecli_ctrlstatus_null_ctx_typed_failure(void)
|
||||
{
|
||||
xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef;
|
||||
int rc = xcecli_ctrlstatus(NULL, &status);
|
||||
/* typed negative, not a partial/empty success */
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
printf("test_xcecli_ctrlstatus_null_ctx_typed_failure: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Suite entry point ──────────────────────────────────────────────────── */
|
||||
|
||||
void test_ctrlstatus(void)
|
||||
{
|
||||
RUN_TEST(test_ctrlstatus_roundtrip_all_fields);
|
||||
RUN_TEST(test_ctrlstatus_roundtrip_unknown_primary);
|
||||
RUN_TEST(test_xcecli_ctrlstatus_null_ctx_typed_failure);
|
||||
}
|
||||
|
|
@ -427,6 +427,36 @@ int xce_clusterconf(xce_ctx_t* ctx, xce_clusterconf_t** conf)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int xce_ctrlstatus(xce_ctx_t* ctx, xce_ctrlstatus_t** status)
|
||||
{
|
||||
int rc, st;
|
||||
|
||||
if ((ctx == NULL) || (status == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (ctx->state != XCE_CTX_RUN) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
rc = xce_ctrlstatus_req(ctx->ep);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = cbuf_get(&(ctx->wbuf), &st);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (st < 0) {
|
||||
return st;
|
||||
}
|
||||
|
||||
MOVE_PTR((*status), ctx->reply.u.ctrlstatus);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xce_update_resvals(xce_ctx_t* ctx, int count, xce_resval_t** vals)
|
||||
{
|
||||
int rc;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@ static int handle_reply(xce_ctx_t* ctx, xce_reply_t* reply)
|
|||
rc = 1;
|
||||
}
|
||||
break;
|
||||
case OPCode_ctrlstatus:
|
||||
rc = -1;
|
||||
if (reply->data_which == RequestReply_data_ctrlstatus) {
|
||||
MOVE_PTR(ctx->reply.u.ctrlstatus, reply->data.ctrlstatus);
|
||||
rc = 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
rc = -ENOENT;
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue