feat(controller-status): xcecli_ctrlstatus_primary authoritative reach-the-primary read (task 3.2)
This commit is contained in:
parent
691cd9199e
commit
03813bf560
4 changed files with 291 additions and 2 deletions
128
src/lib/client.c
128
src/lib/client.c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue