feat(controller-status): wait-primary uses authoritative ctrlstatus_primary probe (task 4.2)

This commit is contained in:
Rongsong Shen 2026-06-20 05:46:24 +00:00
parent 9e87d2c92e
commit 6747304184
2 changed files with 45 additions and 24 deletions

View file

@ -1083,23 +1083,28 @@ int cmd_expect_job(int argc, char* argv[])
return 0;
}
/* `wait-primary [timeout_ms]` (xce-test-e2e-primitives task 6.1, Req 4.1-4.4,
* design.md cmd_wait_primary).
/* `wait-primary [timeout_ms]` (controller-status task 4.2, Req 9.1-9.4;
* originally xce-test-e2e-primitives task 6.1; design.md "cmd_wait_primary probe
* swap — State contract").
*
* Blocks until a cheap, failover-aware client op succeeds, or the timeout
* elapses. The probe is the print-free joblist_query(NULL, NULL, 0): it drives
* active_ctx + libxce master-rotation/failover and returns a job count (>= 0)
* only when a primary actually served the request, or < 0 on error (Req 4.1).
* Blocks until the AUTHORITATIVE controller status probe reaches the serving
* primary, or the timeout elapses. The probe is xcecli_ctrlstatus_primary: it
* drives active_ctx + libxce master-rotation/failover and returns 0 ONLY when a
* reply with responderRole == PRIMARY was obtained (it internally follows the
* redirect to the primary), or a typed negative when no primary answered within
* its budget (Req 9.1). A standby/hint answer is NOT success — the probe keeps
* polling until the primary itself answers (this avoids a false success on a
* standby's stale primary view after a primary death).
*
* On the first successful probe we return 0 (Req 4.2). Otherwise we sleep a
* short poll interval and retry, re-checking a MONOTONIC deadline between
* attempts so the command returns within a bounded time of the timeout and
* never hangs indefinitely when there is no primary (Req 4.3). The deadline is
* derived from CLOCK_MONOTONIC, never wall-clock, so a clock step cannot skew
* it.
* On the first authoritative probe we print the served primary's identity and
* return 0 (Req 9.2). Otherwise we sleep a short poll interval and retry,
* re-checking a MONOTONIC deadline between attempts so the command returns
* within a bounded time of the timeout and never hangs indefinitely when there
* is no primary (Req 9.3). The deadline is derived from CLOCK_MONOTONIC, never
* wall-clock, so a clock step cannot skew it.
*
* The timeout is argv[1] in milliseconds when supplied, else
* WAIT_PRIMARY_DEFAULT_MS (Req 4.4). A malformed timeout argument is a usage
* WAIT_PRIMARY_DEFAULT_MS (Req 9.4). A malformed timeout argument is a usage
* error (return < 0) rather than a silent fallback to the default. */
#define WAIT_PRIMARY_DEFAULT_MS 30000L /* default max wait when arg omitted */
#define WAIT_PRIMARY_POLL_MS 200L /* sleep between probe attempts */
@ -1149,26 +1154,42 @@ int cmd_wait_primary(int argc, char* argv[])
start_ms = monotonic_ms();
/* The poll probes repeatedly and EXPECTS failure until a primary serves, so
* it must not spam the console: the probe suppresses its own error print
* (quiet=1), and we raise the client log threshold for the duration of the
* loop to silence libxce's per-attempt "xcecli failover: attempt N" INFO
* logs — RESTORED to the user's level on every exit path. */
* it must not spam the console: xcecli_ctrlstatus_primary suppresses its own
* per-attempt output, and we raise the client log threshold for the duration
* of the loop to silence libxce's per-attempt "xcecli failover: attempt N"
* INFO logs — RESTORED to the user's level on every exit path. */
mylog_setlevel((char*)"warn");
/* Probe, then re-check the monotonic deadline between attempts. Each probe
* may itself take time (libxce failover has its own internal retry budget);
* we re-read the clock after every probe returns so the overall loop honors
* the deadline and returns failure in bounded time. */
* may itself take time (libxce failover + redirect-to-primary has its own
* internal retry budget); we re-read the clock after every probe returns so
* the overall loop honors the deadline and returns failure in bounded time. */
for (;;) {
int rc = joblist_query(NULL, NULL, 0, /*quiet=*/1);
xce_ctrlstatus_t* status = NULL;
int rc = xcecli_ctrlstatus_primary(active_ctx->ctx,
&status);
/* Success ONLY on an authoritative reply from the serving primary
* (responderRole == PRIMARY, guaranteed by xcecli_ctrlstatus_primary).
* Print the served primary's identity (the responder IS the primary, so
* primaryName == responderName) and free the reply (Req 9.1, 9.2). */
if (rc == 0) {
const char* primary = (status != NULL)
? (NULLSTR(status->primaryName)
? status->responderName
: status->primaryName)
: NULL;
if (rc >= 0) {
elapsed_ms = monotonic_ms() - start_ms;
mylog_setlevel(g_active_loglevel);
printf("wait-primary: primary serving after %ld ms\n", elapsed_ms);
printf("wait-primary: primary '%s' serving after %ld ms\n",
NULLSTR(primary) ? "unknown" : primary, elapsed_ms);
xcecli_free_ctrlstatus(status);
return 0;
}
/* Non-authoritative / failed probe: nothing to free (the typed-failure
* contract leaves *status untouched), keep polling until the deadline. */
elapsed_ms = monotonic_ms() - start_ms;
if (elapsed_ms >= timeout_ms) {