From 31a54043d5a0ccabc9a2c53cfc5e2119f6eb92b8 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 30 May 2026 12:43:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(election-iomgr-refactor):=20wire=20RDHUP?= =?UTF-8?q?=20failover=20to=20ha=5Fstart=5Felection=5Fasync=20=E2=80=94=20?= =?UTF-8?q?task=205.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/ha.c | 31 +++-- src/controller/ha.h | 16 +++ src/controller/testsuite/test_ha_failover.c | 128 ++++++++++++++++++++ 3 files changed, 168 insertions(+), 7 deletions(-) diff --git a/src/controller/ha.c b/src/controller/ha.c index e9390b1..113f7ea 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -2371,6 +2371,16 @@ ctrl_role_t am_i_primary_quorum(ha_state_t *ha, #ifdef XCE_TESTING int (*ha_tcp_connect_hook)(const char *host, int port) = NULL; + +/* + * ha_rdhup_election_hook: test-only seam for ha_start_election_async in the + * RDHUP failover path. When non-NULL, ha_on_primary_ctrl_fd_rdhup() calls + * this instead of the real ha_start_election_async(). Req 6.3, 6.5, 9.3. + */ +void (*ha_rdhup_election_hook)(ha_state_t *ha, cluster_t *c, + int n_peers, xce_candidate_t **peers, + uint64_t my_config_tag, uint64_t my_lsn, + const char *my_name) = NULL; #endif /* ha_tcp_connect_raw: non-blocking connect + poll, returns connected fd or -errno */ @@ -2551,7 +2561,10 @@ void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c) if (committed > 0) persist_truncate_wal_above(&c->persist, committed); - /* Fire election — am_i_primary_quorum handles the vote and role assignment */ + /* Fire async election — ha_start_election_async is non-blocking (Req 6.3, 6.5). + * ha->phase2_done==1 at this point (set after initial startup election), so + * ha_elect_evaluate_and_signal will skip cluster_init_phase2 and go directly + * to CTRL_PRIMARY_COMMITTING (Req 9.3). */ if (c->myconf && c->myconf->peers_cfg.candidates_count > 0) { const xce_cluster_config_t *cfg = &c->myconf->peers_cfg; int n = cfg->candidates_count > 0 ? cfg->candidates_count - 1 : 0; @@ -2565,12 +2578,16 @@ void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c) uint64_t my_tag = c->myconf->config_version_tag; uint64_t my_lsn = atomic_load_explicit(&c->persist.committed_lsn, memory_order_acquire); - ctrl_role_t new_role = am_i_primary_quorum(ha, np, peer_ptrs, my_tag, my_lsn, - c->myconf ? c->myconf->name : ""); - if (new_role == CTRL_PRIMARY) - INFO("HA: RDHUP recovery → CTRL_PRIMARY (Req 6.8)"); - else - INFO("HA: RDHUP recovery → CTRL_STANDBY, reconnecting (Req 6.8)"); + const char *my_name = c->myconf ? c->myconf->name : ""; +#ifdef XCE_TESTING + if (ha_rdhup_election_hook != NULL) { + ha_rdhup_election_hook(ha, c, np, peer_ptrs, my_tag, my_lsn, my_name); + } else { + ha_start_election_async(ha, c, np, peer_ptrs, my_tag, my_lsn, my_name); + } +#else + ha_start_election_async(ha, c, np, peer_ptrs, my_tag, my_lsn, my_name); +#endif } } diff --git a/src/controller/ha.h b/src/controller/ha.h index 5d67239..38bfe23 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -555,6 +555,22 @@ uint64_t ha_trigger_manual_failover_for_test(ha_state_t *ha); * the real connect() path. Returns connected fd on success, -errno on failure. */ extern int (*ha_tcp_connect_hook)(const char *host, int port); + +/* + * ha_rdhup_election_hook: test-only seam for ha_start_election_async in the + * RDHUP failover path. + * + * When non-NULL, ha_on_primary_ctrl_fd_rdhup() calls this INSTEAD OF the real + * ha_start_election_async() so unit tests can verify the RDHUP path triggers + * async re-election without requiring a live iomgr/delaymgr. + * + * The hook receives the same arguments that would be passed to ha_start_election_async. + * Requirements: 6.3, 6.5, 9.3. + */ +extern void (*ha_rdhup_election_hook)(ha_state_t *ha, cluster_t *c, + int n_peers, xce_candidate_t **peers, + uint64_t my_config_tag, uint64_t my_lsn, + const char *my_name); #endif #endif /* HA_H */ diff --git a/src/controller/testsuite/test_ha_failover.c b/src/controller/testsuite/test_ha_failover.c index cdcc64e..249e22d 100644 --- a/src/controller/testsuite/test_ha_failover.c +++ b/src/controller/testsuite/test_ha_failover.c @@ -25,6 +25,8 @@ #include #include "ha.h" +#include "xce/cluster_def.h" /* full cluster_t layout for task-5.2 tests */ +#include "myconf.h" /* myconf_t for task-5.2 tests */ /* ── Forward declarations — function-under-test (not yet implemented) ────── */ @@ -210,6 +212,130 @@ static void test_rdhup_on_minus_one_fd_is_safe(void) printf("test_rdhup_on_minus_one_fd_is_safe: PASS\n"); } +/* ── Task 5.2: RDHUP → CTRL_ELECTING → ha_start_election_async ─────────── */ + +/* + * ha_rdhup_election_hook: XCE_TESTING seam — set in ha.c under XCE_TESTING. + * When non-NULL, ha_on_primary_ctrl_fd_rdhup calls this instead of the real + * ha_start_election_async. We use it to verify the RDHUP path triggers + * async re-election (Req 6.3, 6.5, 9.3). + */ +extern void (*ha_rdhup_election_hook)(ha_state_t *ha, cluster_t *c, + int n_peers, xce_candidate_t **peers, + uint64_t my_config_tag, uint64_t my_lsn, + const char *my_name); + +/* Counters set by the hook so the test can inspect the call */ +static int g_rdhup_hook_called = 0; +static int g_rdhup_hook_n_peers = -1; +static ha_state_t *g_rdhup_hook_ha = NULL; + +static void rdhup_election_spy(ha_state_t *ha, cluster_t *c, + int n_peers, xce_candidate_t **peers, + uint64_t my_config_tag, uint64_t my_lsn, + const char *my_name) +{ + (void)c; (void)peers; (void)my_config_tag; (void)my_lsn; (void)my_name; + g_rdhup_hook_called++; + g_rdhup_hook_n_peers = n_peers; + g_rdhup_hook_ha = ha; +} + +/* + * test_rdhup_calls_start_election_async: + * Req 6.3: heartbeat timeout → ELECTING → async election fires. + * Req 9.3: RDHUP path uses ha_start_election_async (not am_i_primary_quorum). + * + * We supply a minimal cluster_t with 2 candidates so the RDHUP handler + * builds a peer array and calls ha_start_election_async via the hook. + * The test also verifies role is CTRL_ELECTING after the handler. + * + * myconf_t.name is char* — use a string literal. + * myhost == NULL → self-name compare falls back to "" → both candidates are + * included in peer_ptrs (np == 2). That is fine for verifying the hook fires. + */ +static void test_rdhup_calls_start_election_async(void) +{ + /* Minimal myconf with 2 candidates (self + 1 peer). + * myconf_t.name is char* — point at a static string. */ + myconf_t myconf; + memset(&myconf, 0, sizeof(myconf)); + myconf.name = "ctrl-a"; + myconf.peers_cfg.candidates_count = 2; + snprintf(myconf.peers_cfg.candidates[0].name, + sizeof(myconf.peers_cfg.candidates[0].name), "ctrl-a"); + myconf.peers_cfg.candidates[0].port = 7001; + snprintf(myconf.peers_cfg.candidates[1].name, + sizeof(myconf.peers_cfg.candidates[1].name), "ctrl-b"); + myconf.peers_cfg.candidates[1].port = 7002; + + /* Minimal cluster_t — ha embedded, myhost NULL (unit test mode) */ + cluster_t c; + memset(&c, 0, sizeof(c)); + c.myconf = &myconf; + /* Initialise the embedded ha_state_t directly */ + TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &g_cfg, 0 /* priority=0 */)); + ha_do_demote(&c.ha); + + /* Mark phase2 done — simulates a surviving standby after first election */ + c.ha.phase2_done = 1; + + /* Set up a socketpair as the "primary_ctrl_fd" */ + int sv[2]; + TEST_ASSERT_EQUAL_INT(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv)); + c.ha.primary_ctrl_fd = sv[0]; + + /* Install spy hook */ + g_rdhup_hook_called = 0; + g_rdhup_hook_n_peers = -1; + g_rdhup_hook_ha = NULL; + ha_rdhup_election_hook = rdhup_election_spy; + + /* Fire RDHUP with the stub cluster */ + ha_on_primary_ctrl_fd_rdhup(&c.ha, &c); + + /* Restore hook */ + ha_rdhup_election_hook = NULL; + + /* Verify: role must be CTRL_ELECTING */ + TEST_ASSERT_EQUAL_INT(CTRL_ELECTING, (int)atomic_load(&c.ha.role)); + + /* Verify: fd must be closed and set to -1 */ + TEST_ASSERT_EQUAL_INT(-1, c.ha.primary_ctrl_fd); + + /* Verify: ha_start_election_async (hook) was called exactly once */ + TEST_ASSERT_EQUAL_INT(1, g_rdhup_hook_called); + + /* Verify: n_peers > 0 (loop ran; myhost==NULL so both candidates included) */ + TEST_ASSERT_GREATER_THAN_INT(0, g_rdhup_hook_n_peers); + + close(sv[1]); /* sv[0] was closed by the RDHUP handler */ + ha_fini(&c.ha); + printf("test_rdhup_calls_start_election_async: PASS\n"); +} + +/* + * test_rdhup_phase2_done_preserved: + * Req 9.3: phase2_done flag must remain 1 after RDHUP so ha_elect_evaluate_and_signal + * skips cluster_init_phase2 on the failover election. + */ +static void test_rdhup_phase2_done_preserved(void) +{ + ha_state_t ha; + TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 0)); + ha_do_demote(&ha); + ha.phase2_done = 1; /* set by first-boot election */ + + /* Fire RDHUP in NULL-cluster mode (unit-test shortcircuit) */ + ha_on_primary_ctrl_fd_rdhup(&ha, NULL); + + /* phase2_done must still be 1 — RDHUP does NOT reset it */ + TEST_ASSERT_EQUAL_INT(1, ha.phase2_done); + + ha_fini(&ha); + printf("test_rdhup_phase2_done_preserved: PASS\n"); +} + /* ── Test 6: symbol existence check ─────────────────────────────────────── */ static void test_ha_failover_symbols_exist(void) @@ -242,5 +368,7 @@ void test_ha_failover(void) RUN_TEST(test_heartbeat_timeout_fires_failover); RUN_TEST(test_rdhup_on_minus_one_fd_is_safe); RUN_TEST(test_ha_failover_symbols_exist); + RUN_TEST(test_rdhup_calls_start_election_async); + RUN_TEST(test_rdhup_phase2_done_preserved); UNITY_END(); }