feat: ha_should_redirect + join.c HA redirect/nomaster guard (P1-6)

Add redirect helpers to ha.c/ha.h so a standby controller never accepts
join requests as if it were the primary:

- ha_should_redirect(): returns 1 when role is STANDBY and primary_host/port
  are known — caller should send E_XCE_REDIRECT.
- ha_no_primary_known(): returns 1 when role is STANDBY but primary address
  is not yet known — caller should send E_XCE_NOMASTER (retry later).
- Add E_XCE_NOMASTER and E_XCE_REDIRECT to errnox.h.
- Wire both guards into handle_join_() in join.c (before the role switch)
  so all join types (worker, scheduler, apiserver, client) are protected.
- Full redirect response sends status=-E_XCE_REDIRECT with nodata body;
  the primary host:port are logged so operators can trace misrouted clients.
  Full Cap'n Proto RedirectInfo response body is deferred to a follow-up
  (schema union would require regenerating capnp bindings).

5 new TDD tests — 110 total, 0 failures.
This commit is contained in:
Rongsong Shen 2026-05-07 15:11:53 +00:00
parent c627f69d50
commit f5b0ac0af3
7 changed files with 191 additions and 0 deletions

View file

@ -186,6 +186,38 @@ void ha_fini(ha_state_t *ha)
pthread_mutex_destroy(&ha->lock);
}
/* ── P1-6: Redirect helpers ────────────────────────────────── */
/*
* ha_should_redirect: returns 1 when this standby knows the primary's
* address and should redirect incoming join requests there.
* Returns 0 for PRIMARY role, or for STANDBY with no known primary yet.
*/
int ha_should_redirect(const ha_state_t *ha)
{
ctrl_role_t role =
(ctrl_role_t)atomic_load_explicit(&ha->role, memory_order_acquire);
if (role == CTRL_PRIMARY)
return 0;
if (ha->primary_host[0] == '\0' || ha->primary_port == 0)
return 0;
return 1;
}
/*
* ha_no_primary_known: returns 1 when this node is a STANDBY but does not
* yet know the primary's address. Callers should reply E_XCE_NOMASTER so
* the client retries rather than treating the error as permanent.
*/
int ha_no_primary_known(const ha_state_t *ha)
{
ctrl_role_t role =
(ctrl_role_t)atomic_load_explicit(&ha->role, memory_order_acquire);
if (role == CTRL_PRIMARY)
return 0;
return (ha->primary_host[0] == '\0' || ha->primary_port == 0) ? 1 : 0;
}
/*
* P1-5: cluster_t now embeds ha_state_t ha, so the three functions below
* can dereference c->ha.