From 9e87d2c92ea2bffb6532cf2abf3b06065d611a52 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 20 Jun 2026 05:40:15 +0000 Subject: [PATCH] feat(controller-status): ctrlstatus client command (task 4.1) --- .kiro/specs/controller-status/tasks.md | 2 +- src/controller/testsuite/test.c | 122 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/controller-status/tasks.md b/.kiro/specs/controller-status/tasks.md index 36a4dcbc..58c4a717 100644 --- a/.kiro/specs/controller-status/tasks.md +++ b/.kiro/specs/controller-status/tasks.md @@ -43,7 +43,7 @@ - _Depends: 3.1_ - [ ] 4. Test client command + wait-primary integration -- [ ] 4.1 Add the `ctrlstatus` client command +- [x] 4.1 Add the `ctrlstatus` client command - Add a client command that issues the any-node status call and prints the fields in human-readable form: the current primary, the answering node framed as "answered by ()" distinct from the primary, the configured controllers and count, the loaded config version and committed config version, the content checksum as hex, term, commit/apply positions, fenced state, and cluster name; render an empty primary as "unknown". The command exit status reflects success or failure of the query. - Observable: piping `ctrlstatus` through the client prints the status fields (with "answered by" and "unknown" rendering) and exits zero on success / non-zero with the error printed on failure. - _Requirements: 1.3, 3.3, 5.1, 5.2, 8.1, 8.2, 8.3_ diff --git a/src/controller/testsuite/test.c b/src/controller/testsuite/test.c index 296c8ec4..99be18c0 100644 --- a/src/controller/testsuite/test.c +++ b/src/controller/testsuite/test.c @@ -120,6 +120,7 @@ extern int cmd_echo(int, char*[]); extern int cmd_expect_jobs(int, char*[]); extern int cmd_expect_job(int, char*[]); extern int cmd_wait_primary(int, char*[]); +extern int cmd_ctrlstatus(int, char*[]); static int worker_house_keeping(void*); /* Session verdict / loop state (xce-test-e2e-primitives task 1.1). @@ -208,6 +209,7 @@ static command_t client_commands[] = { { "expect-job", cmd_expect_job, "assert a JobId is present" }, { "wait-primary", cmd_wait_primary, "wait until a primary serves [timeout_ms]" }, + { "ctrlstatus", cmd_ctrlstatus, "show controller/cluster status" }, { "shutdown", cmd_shutdown, "shutdown xce controller" }, { "jobrun", cmd_jobrun, "run a job" }, { "jobsub", cmd_jobsub, "submit a job" }, @@ -1191,6 +1193,126 @@ int cmd_wait_primary(int argc, char* argv[]) } } +/* Map a ctrl_role_t raw value (CtrlStatus.responderRole, see ha.h:60-63 and + * design.md "Schema — reply payload") to a display string. PRIMARY==0, + * STANDBY==1, INIT==2, ELECTING==3; anything else is reported verbatim so an + * unexpected role is never silently rendered as a known one. */ +static const char* ctrlstatus_role_str(uint8_t role) +{ + switch (role) { + case 0: + return "primary"; + case 1: + return "standby"; + case 2: + return "initializing"; + case 3: + return "electing"; + default: + return "unknown-role"; + } +} + +/* `ctrlstatus` (controller-status task 4.1, Req 1.3/3.3/5.1/5.2/8.1/8.2/8.3, + * design.md "cmd_ctrlstatus (test client) — State contract"). + * + * Issues the any-node status read (xcecli_ctrlstatus, is_write=false: whatever + * controller answers — primary or standby hint view — rides the configured + * failover/rotation) and prints the identity/health fields in human-readable + * form. + * + * On failure (no reachable controller etc.) it prints a typed error and returns + * the negative rc so the session records the command as failed and exits + * non-zero (Req 8.3). On success it prints the fields and returns 0 (Req 8.2). */ +int cmd_ctrlstatus(int argc, char* argv[]) +{ + xce_ctrlstatus_t* status = NULL; + int rc; + int i; + + (void)argv; + + if (argc != 1) { + printf("ctrlstatus\n"); + return -EINVAL; + } + + rc = xcecli_ctrlstatus(active_ctx->ctx, &status); + + if (rc < 0) { + char ebuf[128]; + + ebuf[0] = '\0'; + xce_strerror_r(-rc, ebuf, sizeof(ebuf)); + SET_ERROR(-rc); + + switch (-rc) { + case E_XCE_NOMASTER: + fprintf(stderr, "ctrlstatus failed: no-master (%s)\n", ebuf); + break; + case E_XCE_NOT_READY: + fprintf(stderr, "ctrlstatus failed: not-ready (%s)\n", ebuf); + break; + case ETIMEDOUT: + fprintf(stderr, "ctrlstatus failed: timeout (%s)\n", ebuf); + break; + default: + fprintf(stderr, "ctrlstatus failed: %s\n", ebuf); + break; + } + + return rc; + } + + /* Current primary (Req 2.x): an empty primaryName means no elected/known + * primary — render the well-defined "unknown" (Req 1.3/2.2/2.3) rather than + * an empty or stale value. */ + printf("Primary: %s\n", + NULLSTR(status->primaryName) ? "unknown" : status->primaryName); + + /* Answering node + role, framed as a property of the responder and kept + * DISTINCT from the authoritative Primary line above (Req 3.1/3.2/3.3): a + * transient responder role (electing/initializing) must not be misread as + * the cluster's primary state. */ + printf("answered by %s (%s)\n", + NULLSTR(status->responderName) ? "unknown" : status->responderName, + ctrlstatus_role_str(status->responderRole)); + + printf("Cluster: %s\n", + NULLSTR(status->clusterName) ? "unknown" : status->clusterName); + + /* Configured controller topology + count (Req 4.1/4.2). */ + printf("Controllers (%u):\n", status->peerCount); + for (i = 0; i < status->peers.count; i++) { + xce_ctrlpeer_t* peer = status->peers.list[i]; + + if (peer == NULL) { + continue; + } + + printf("\t%s\t%s:%u\n", NULLSTR(peer->name) ? "?" : peer->name, + NULLSTR(peer->host) ? "?" : peer->host, peer->port); + } + + /* Config version + committed config version (Req 5.1) and the content + * checksum as the hex string the controller already exposes (Req 5.2). */ + printf("configVersion: %llu\n", (unsigned long long)status->configVersion); + printf("committedConfigVersion: %llu\n", + (unsigned long long)status->committedConfigVersion); + printf("configChecksum: %s\n", + NULLSTR(status->configChecksum) ? "unknown" : status->configChecksum); + + /* HA health / progress (Req 6.x). */ + printf("term: %llu\n", (unsigned long long)status->term); + printf("committedLsn: %llu\n", (unsigned long long)status->committedLsn); + printf("appliedLsn: %llu\n", (unsigned long long)status->appliedLsn); + printf("fenced: %s\n", status->fenced ? "true" : "false"); + + xcecli_free_ctrlstatus(status); + + return 0; +} + static void print_jobinfo(xce_job_t* job) { char idstring[128];