feat(controller-status): xcecli_ctrlstatus_primary authoritative reach-the-primary read (task 3.2)

This commit is contained in:
Rongsong Shen 2026-06-20 05:32:18 +00:00
parent 691cd9199e
commit 03813bf560
4 changed files with 291 additions and 2 deletions

View file

@ -2,6 +2,8 @@
#define _CLIENT_H_ 1
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "netio.h"
@ -54,6 +56,29 @@ 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**);
/*
* xcecli_ctrlstatus_primary (controller-status task 3.2): AUTHORITATIVE
* controller status read. Returns ONLY a reply whose responder role is the
* primary. When the first answer is from a non-primary, it follows the redirect
* (targets the reported primary via the reply's peer list, else rotates the
* configured controller list) until the primary answers or the failover budget
* is exhausted. On budget exhaustion / no reachable primary it returns a typed
* negative and NEVER hands back a non-primary hint reply (Req 9.1, 9.2). The
* any-node hint view is still available via xcecli_ctrlstatus above.
*/
extern int xcecli_ctrlstatus_primary(xcecli_ctx_t, xce_ctrlstatus_t**);
/*
* Pure decision helpers backing the authoritative read (no I/O — unit-testable
* and exposed for tests). xcecli_ctrlstatus_is_authoritative returns true iff
* the reply's responderRole is the primary role (ctrl_role_t PRIMARY == 0).
* xcecli_ctrlstatus_primary_addr maps the reply's primaryName to its host:port
* via the reply's peers list: returns 0 and fills host/port on a hit, -ENOENT
* when the primary is unknown/absent from peers, -EINVAL on a bad argument.
*/
extern bool xcecli_ctrlstatus_is_authoritative(const xce_ctrlstatus_t*);
extern int xcecli_ctrlstatus_primary_addr(const xce_ctrlstatus_t*,
char* host_out, size_t host_sz,
uint16_t* port_out);
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*);

View file

@ -1,6 +1,7 @@
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include "client.h"
@ -9,6 +10,7 @@
#include "mylock.h"
#include "request.h"
#include "xce/errnox.h" /* E_XCE_NOT_READY */
#include "xce/types.h"
#include "xceapi.h"
#include "xcectx.h"
@ -336,6 +338,132 @@ int xcecli_ctrlstatus(xcecli_ctx_t c, xce_ctrlstatus_t** status)
xce_ctrlstatus(&(ctx->ctx), status));
}
/*
* The primary's responder role on the wire is ctrl_role_t PRIMARY. That enum
* lives in the controller-private ha.h (CTRL_PRIMARY == 0) and is intentionally
* not exposed to libxce; the wire ordinal is part of the ctrlstatus reply
* contract (controller/ctrlstatus.c stamps responderRole = ha->role). We pin it
* here as a named constant so the comparison reads intentionally rather than as
* a bare 0. Any reorder of ctrl_role_t is a Revalidation Trigger for this spec.
*/
#define XCECLI_CTRLSTATUS_ROLE_PRIMARY ((uint8_t)0)
bool xcecli_ctrlstatus_is_authoritative(const xce_ctrlstatus_t* status)
{
if (status == NULL) {
return false;
}
return status->responderRole == XCECLI_CTRLSTATUS_ROLE_PRIMARY;
}
int xcecli_ctrlstatus_primary_addr(const xce_ctrlstatus_t* status,
char* host_out, size_t host_sz,
uint16_t* port_out)
{
int i;
if ((status == NULL) || (host_out == NULL) || (host_sz == 0)
|| (port_out == NULL)) {
return -EINVAL;
}
/* No known primary (empty/NULL primaryName) ⇒ nothing to target; the caller
* falls back to plain master rotation (Req 2.3 unknown-primary). */
if ((status->primaryName == NULL) || (status->primaryName[0] == '\0')) {
return -ENOENT;
}
/* Map the reported primaryName to its configured address via the reply's
* peers list. The list is a small bounded set (configured controllers). */
for (i = 0; i < status->peers.count; i++) {
xce_ctrlpeer_t* p = status->peers.list[i];
if ((p == NULL) || (p->name == NULL) || (p->host == NULL)) {
continue;
}
if (strcmp(p->name, status->primaryName) != 0) {
continue;
}
if ((p->host[0] == '\0') || (p->port == 0)) {
/* named primary has no usable address — treat as absent */
break;
}
strncpy(host_out, p->host, host_sz - 1);
host_out[host_sz - 1] = '\0';
*port_out = (uint16_t)p->port;
return 0;
}
/* primaryName not present in peers (or no usable address): fall back to
* rotation rather than targeting a bogus address. */
return -ENOENT;
}
int xcecli_ctrlstatus_primary(xcecli_ctx_t c, xce_ctrlstatus_t** status)
{
api_ctx_t* ctx = (api_ctx_t*)c;
xce_ctrlstatus_t* hint;
int rc;
int attempt;
int max_attempts;
if ((c == NULL) || (status == NULL)) {
return -EINVAL;
}
/* Bound the redirect loop by the configured failover retry budget so we
* never spin unbounded chasing a never-appearing primary (each inner
* xcecli_ctrlstatus already runs its own full reach-some-node failover). */
max_attempts = ctx->failover.retry.max_retries;
if (max_attempts < 1) {
max_attempts = 1;
}
for (attempt = 0; attempt < max_attempts; attempt++) {
hint = NULL;
rc = xcecli_ctrlstatus(c, &hint);
if (rc < 0) {
/* no reachable controller within the inner budget ⇒ typed failure;
* *status untouched (never a hint reply as the result, Req 9.2). */
return rc;
}
if (xcecli_ctrlstatus_is_authoritative(hint)) {
/* the primary itself answered — authoritative result */
*status = hint;
return 0;
}
/* Non-authoritative HINT (standby/electing/init). Direct the next query
* at the reported primary if we can map it to an address; otherwise let
* the next xcecli_ctrlstatus rotate the configured master list. The
* redirect hint is consumed one-shot by xce_failover_next_target. */
{
char phost[XCE_MAXHOSTNAME_LEN];
uint16_t pport = 0;
if (xcecli_ctrlstatus_primary_addr(hint, phost, sizeof(phost),
&pport)
== 0) {
mylock_lock(&ctx->failover.lock);
xce_ctx_capture_redirect_hint(&ctx->ctx, phost, pport);
mylock_unlock(&ctx->failover.lock);
}
}
/* Free the non-authoritative interim reply before the next attempt — no
* leak, and we never return a hint as the authoritative result. */
xcecli_free_ctrlstatus(hint);
}
/* Budget exhausted without the primary answering: typed failure, *status
* untouched (Req 9.2 — never return a non-primary reply as authoritative).
*/
return -E_XCE_NOT_READY;
}
int xcecli_notify_jobfinish(xcecli_ctx_t c, xce_jobfinish_t* jfinish)
{
api_ctx_t* ctx = (api_ctx_t*)c;

View file

@ -21,6 +21,7 @@
#include <unity/unity.h>
#include "client.h"
#include "config.h" /* XCE_MAXHOSTNAME_LEN */
#include "request.h"
#include "xce/types.h"
@ -195,12 +196,141 @@ static void test_ctrlstatus_roundtrip_unknown_primary(void)
static void test_xcecli_ctrlstatus_null_ctx_typed_failure(void)
{
xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef;
int rc = xcecli_ctrlstatus(NULL, &status);
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");
}
/* ── authoritative (primary-only) read: task 3.2 ─────────────────────────── */
/* The authoritative call must reject a NULL/unconnected ctx with the same
* typed negative as the any-node read — it must NEVER hand back a hint reply
* as the authoritative result (Req 9.2). The `status` out-param is left
* untouched on failure (it still holds the poisoned sentinel below). */
static void test_xcecli_ctrlstatus_primary_null_ctx_typed_failure(void)
{
xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef;
int rc = xcecli_ctrlstatus_primary(NULL, &status);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
/* never returned a hint reply as the authoritative result */
TEST_ASSERT_EQUAL_PTR((xce_ctrlstatus_t*)0xdeadbeef, status);
printf("test_xcecli_ctrlstatus_primary_null_ctx_typed_failure: PASS\n");
}
/* NULL status out-param is also a typed failure (boundary validation). */
static void test_xcecli_ctrlstatus_primary_null_out_typed_failure(void)
{
int rc = xcecli_ctrlstatus_primary(NULL, NULL);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_xcecli_ctrlstatus_primary_null_out_typed_failure: PASS\n");
}
/* The pure authoritative-decision helper: a reply is authoritative iff the
* responder's role is PRIMARY (ctrl_role_t PRIMARY == 0). A standby (1),
* init (2), or electing (3) responder is a non-authoritative HINT — the call
* must keep following the redirect (Req 9.1, 2.3, 3.2). */
static void test_ctrlstatus_is_authoritative_decision(void)
{
xce_ctrlstatus_t reply;
memset(&reply, 0, sizeof(reply));
reply.responderRole = 0; /* PRIMARY */
TEST_ASSERT_TRUE(xcecli_ctrlstatus_is_authoritative(&reply));
reply.responderRole = 1; /* STANDBY */
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
reply.responderRole = 2; /* INIT */
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
reply.responderRole = 3; /* ELECTING */
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
/* NULL reply is never authoritative (defensive). */
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(NULL));
printf("test_ctrlstatus_is_authoritative_decision: PASS\n");
}
/* The pure next-target helper: given a standby's hint reply, map the reported
* primaryName to its host:port via the reply's peers list so the next query
* can target the primary directly (the redirect step of Req 9.1). */
static void test_ctrlstatus_primary_addr_from_peers(void)
{
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 hint;
memset(&hint, 0, sizeof(hint));
hint.responderName = "ctrl-a";
hint.responderRole = 1; /* STANDBY */
hint.primaryName = "ctrl-b"; /* the standby knows ctrl-b is primary */
hint.peers.count = 2;
hint.peers.list = peerlist;
hint.peerCount = 2;
char host[XCE_MAXHOSTNAME_LEN];
uint16_t port = 0;
int rc = xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), &port);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_STRING("10.0.0.2", host);
TEST_ASSERT_EQUAL_UINT16(9992, port);
printf("test_ctrlstatus_primary_addr_from_peers: PASS\n");
}
/* When the standby reports no known primary (empty primaryName) or the named
* primary is not in the peers list, the address lookup fails (-ENOENT) so the
* caller falls back to plain master rotation rather than targeting a bogus
* address (Req 2.3 unknown-primary, redirect best-effort). */
static void test_ctrlstatus_primary_addr_unknown_or_missing(void)
{
xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 };
xce_ctrlpeer_t* peerlist[1] = { &p0 };
xce_ctrlstatus_t hint;
memset(&hint, 0, sizeof(hint));
hint.responderName = "ctrl-a";
hint.responderRole = 3; /* ELECTING */
hint.peers.count = 1;
hint.peers.list = peerlist;
hint.peerCount = 1;
char host[XCE_MAXHOSTNAME_LEN];
uint16_t port = 7;
/* unknown primary (empty) */
hint.primaryName = "";
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
&hint, host, sizeof(host), &port));
/* NULL primary */
hint.primaryName = NULL;
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
&hint, host, sizeof(host), &port));
/* named primary not present in peers */
hint.primaryName = "ctrl-z";
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
&hint, host, sizeof(host), &port));
/* NULL/zero args are rejected */
hint.primaryName = "ctrl-a";
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr(
NULL, host, sizeof(host), &port));
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr(
&hint, NULL, sizeof(host), &port));
TEST_ASSERT_EQUAL_INT(
-EINVAL, xcecli_ctrlstatus_primary_addr(&hint, host, 0, &port));
TEST_ASSERT_EQUAL_INT(
-EINVAL,
xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), NULL));
printf("test_ctrlstatus_primary_addr_unknown_or_missing: PASS\n");
}
/* ── Suite entry point ──────────────────────────────────────────────────── */
void test_ctrlstatus(void)
@ -208,4 +338,9 @@ 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);
RUN_TEST(test_xcecli_ctrlstatus_primary_null_ctx_typed_failure);
RUN_TEST(test_xcecli_ctrlstatus_primary_null_out_typed_failure);
RUN_TEST(test_ctrlstatus_is_authoritative_decision);
RUN_TEST(test_ctrlstatus_primary_addr_from_peers);
RUN_TEST(test_ctrlstatus_primary_addr_unknown_or_missing);
}