feat(controller-status): ctrlstatus client command (task 4.1)

This commit is contained in:
Rongsong Shen 2026-06-20 05:40:15 +00:00
parent 03813bf560
commit 9e87d2c92e
2 changed files with 123 additions and 1 deletions

View file

@ -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 <node> (<role>)" 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_

View file

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