feat(election-iomgr-refactor): wire RDHUP failover to ha_start_election_async — task 5.2

This commit is contained in:
Rongsong Shen 2026-05-30 12:43:02 +00:00
parent 2e5681387a
commit 31a54043d5
3 changed files with 168 additions and 7 deletions

View file

@ -25,6 +25,8 @@
#include <unity/unity.h>
#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();
}