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

@ -21,6 +21,7 @@ extern void test_e2e_gc(void);
extern void test_e2e_capnp(void);
extern void test_ha_role(void);
extern void test_ha_cluster(void);
extern void test_ha_redirect(void);
void setUp(void) { return; }
@ -111,6 +112,7 @@ int main(int argc, char* argv[])
TEST(test_e2e_capnp);
TEST(test_ha_role);
TEST(test_ha_cluster);
TEST(test_ha_redirect);
return UNITY_END();
}

View file

@ -0,0 +1,110 @@
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
#include "ha.h"
/* ── helpers ────────────────────────────────────────────────── */
static ha_config_t valid_cfg(void)
{
ha_config_t cfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.max_promotion_retries = 5,
};
return cfg;
}
/* ── tests ──────────────────────────────────────────────────── */
static void test_primary_does_not_redirect(void)
{
ha_state_t ha;
ha_config_t cfg = valid_cfg();
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
/* Explicitly set PRIMARY role */
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
TEST_ASSERT_EQUAL_INT(0, ha_should_redirect(&ha));
ha_fini(&ha);
printf("test_primary_does_not_redirect: PASS\n");
}
static void test_standby_redirects_when_primary_known(void)
{
ha_state_t ha;
ha_config_t cfg = valid_cfg();
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 1));
/* Default role after init is STANDBY; set primary info */
strncpy(ha.primary_host, "10.0.1.1", sizeof(ha.primary_host) - 1);
ha.primary_port = 7000;
TEST_ASSERT_EQUAL_INT(1, ha_should_redirect(&ha));
ha_fini(&ha);
printf("test_standby_redirects_when_primary_known: PASS\n");
}
static void test_standby_no_redirect_when_no_primary_known(void)
{
ha_state_t ha;
ha_config_t cfg = valid_cfg();
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 1));
/* primary_host is empty after init — no known primary */
TEST_ASSERT_EQUAL_INT(0, ha_should_redirect(&ha));
TEST_ASSERT_EQUAL_INT(1, ha_no_primary_known(&ha));
ha_fini(&ha);
printf("test_standby_no_redirect_when_no_primary_known: PASS\n");
}
static void test_primary_is_not_no_primary_known(void)
{
ha_state_t ha;
ha_config_t cfg = valid_cfg();
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
TEST_ASSERT_EQUAL_INT(0, ha_no_primary_known(&ha));
ha_fini(&ha);
printf("test_primary_is_not_no_primary_known: PASS\n");
}
static void test_standby_no_primary_known_when_port_zero(void)
{
ha_state_t ha;
ha_config_t cfg = valid_cfg();
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 1));
/* Host set but port = 0 — not a valid redirect target */
strncpy(ha.primary_host, "10.0.1.1", sizeof(ha.primary_host) - 1);
ha.primary_port = 0;
TEST_ASSERT_EQUAL_INT(0, ha_should_redirect(&ha));
TEST_ASSERT_EQUAL_INT(1, ha_no_primary_known(&ha));
ha_fini(&ha);
printf("test_standby_no_primary_known_when_port_zero: PASS\n");
}
/* ── suite entry point ─────────────────────────────────────── */
void test_ha_redirect(void)
{
RUN_TEST(test_primary_does_not_redirect);
RUN_TEST(test_standby_redirects_when_primary_known);
RUN_TEST(test_standby_no_redirect_when_no_primary_known);
RUN_TEST(test_primary_is_not_no_primary_known);
RUN_TEST(test_standby_no_primary_known_when_port_zero);
}