fix(controller): standby dispatch-gate rejections now redirect to a known primary (bug-1700, bug-1701)

xceadmin scheduler record status/start/stop against a standby failed
with the bare "controller returned status -10014" (E_XCE_STANDBY) with
no redirect, forcing the caller to guess the primary manually.

bug-1700: myreq_handler's generic ha_dispatch_gate rejection path
(cluster.c) now calls fill_redirect_reply() when rc == -XCE_ERR_STANDBY,
mirroring the same helper OPCode_join's handler already uses --
-XCE_ERR_NOT_READY (CTRL_INIT/CTRL_ELECTING, no settled primary to
redirect to) is deliberately excluded. Landing this alone had zero
observable effect live: ha_state_t.primary_host/primary_port were never
written by any production path on a standby (only test fixtures set
them) -- fill_redirect_reply() always saw an empty/zero redirect target
in a real deployment.

bug-1701: closes that data gap with a pure local lookup, no new wire
field. handle_ctrlhello() (config_repl.c) now resolves the ctrlhello
sender's name against this node's own statically-known peers_cfg
(parsed once from --peers at boot) and populates primary_host/port at
the exact moment a standby first learns who primary is.

Live-verified end to end on a fresh 3-node cluster: xceadmin scheduler
record status against any standby now prints "redirected: <standby> ->
<primary>" and returns the real status, confirmed again after a
re-election changed which node was primary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
Rongsong Shen 2026-08-27 08:25:55 +00:00
parent ac1e312e2f
commit f886c80930
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 305 additions and 0 deletions

View file

@ -366,6 +366,10 @@ void schedadmin_set_timeout_ms_for_test(long ms)
#define SCHEDADMIN_TIMEOUT_MS 5000L
#endif
/* fill_redirect_reply is defined in join.c (no header declaration) --
* mirrors config_repl.c's identical local extern for the same helper. */
extern void fill_redirect_reply(xce_reply_t *reply, const ha_state_t *ha);
static int myreq_handler(vtask_t* task)
{
vtask_data_t* data = (vtask_data_t*)task->data;
@ -408,6 +412,25 @@ static int myreq_handler(vtask_t* task)
if (rc != 0) {
reply.status = rc;
reply.data_which = RequestReply_data_nodata;
/* bug-1700: -XCE_ERR_STANDBY (ha_dispatch_gate's CTRL_STANDBY
* default-case rejection) is the one gate rejection where this node
* may know who the primary is -- populate the redirect hint so
* xceadmin's already-generic client-side follow_redirects() can
* transparently retry there, exactly like OPCode_join's handler
* already does (join.c:277). ha_dispatch_gate's OTHER nonzero
* rejection, -XCE_ERR_NOT_READY (CTRL_INIT/CTRL_ELECTING, and a
* fenced/quorum-less CTRL_PRIMARY), has no settled primary to
* redirect to, so it is deliberately excluded. fill_redirect_reply()
* is safe even when no primary is known yet: ha->primary_host is a
* fixed buffer embedded in ha_state_t (never NULL), so an
* as-yet-unknown primary just yields an empty string / port 0,
* which both the capnp encode path (controller.capnp.c's
* encode_RequestReply, gated on redirect_host != NULL) and the
* client's discovery.py _redirect_target() already treat as
* "no redirect" -- verified via code inspection, not assumed. */
if (rc == -XCE_ERR_STANDBY) {
fill_redirect_reply(&reply, &c->ha);
}
xce_reply(ep, c->myhost, &reply);
xce_free_request(&request);
return rc;