- CRITICAL: single-node shortcircuit enters CTRL_PRIMARY_COMMITTING (not CTRL_PRIMARY directly); phase2 transition deferred to task 4.x - CRITICAL: add n_peers==0 guard in ha_elect_evaluate_and_signal to prevent votes[0] out-of-bounds access - HIGH: wire client_ssl_ctx after endpoint_connect for outgoing votes - HIGH: add timeout_ctx field to ha_elect_ctx_t for task 4.x cancel - HIGH: clarify vote_ep_err_handler ownership (no premature free of vctx; deferred to ha_election_cleanup in task 5.1) - Tests updated: single-node shortcircuit asserts COMMITTING not PRIMARY
199 lines
7.6 KiB
C
199 lines
7.6 KiB
C
/*
|
|
* test_ha_start_election_async.c — Unity tests for task 2.4:
|
|
* ha_start_election_async() non-blocking election initiator.
|
|
*
|
|
* Strategy:
|
|
* - n_peers=1 shortcircuit: no iomgr needed; election completes synchronously
|
|
* via ha_elect_evaluate_and_signal immediately after ha_start_election_async
|
|
* returns (role flips to CTRL_PRIMARY_COMMITTING; task 4.4 committing timeout
|
|
* will later promote to CTRL_PRIMARY for degraded/single-node mode).
|
|
* - Function returns in < 5ms (observable via clock_gettime).
|
|
* - n_peers=1 shortcircuit stores n_peers in ha->n_peers.
|
|
*
|
|
* Observable: function returns in < 5ms; n_peers stored; shortcircuit fires
|
|
* ha_elect_evaluate_and_signal directly (no iomgr/delaymgr needed for n_peers=1).
|
|
*
|
|
* Requirements: 1.3 (non-blocking), 2.1 (single-node shortcircuit), 2.4 (vote send)
|
|
* Boundary: ha.c (election orchestration)
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "ha.h"
|
|
#include "netio.h"
|
|
#include "xce/cluster_def.h"
|
|
#include "delay.h"
|
|
|
|
/* Forward declaration for function under test */
|
|
extern void ha_start_election_async(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);
|
|
|
|
/* ── Minimal ha_config_t for tests ─────────────────────────────────────── */
|
|
|
|
static ha_config_t g_async_cfg = {
|
|
.heartbeat_interval_ms = 3000,
|
|
.heartbeat_timeout_ms = 10000,
|
|
.election_timeout_ms = 2000,
|
|
.probe_connect_timeout_ms = 500,
|
|
.max_promotion_retries = 5,
|
|
.election_jitter_max_ms = 0,
|
|
.election_priority_step_ms = 500,
|
|
.committing_timeout_ms = 10000,
|
|
};
|
|
|
|
/* ── Helper: elapsed ms since a timespec ───────────────────────────────── */
|
|
|
|
static long elapsed_ms(const struct timespec *start)
|
|
{
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
long sec_diff = (long)(now.tv_sec - start->tv_sec);
|
|
long nsec_diff = (long)(now.tv_nsec - start->tv_nsec);
|
|
return sec_diff * 1000L + nsec_diff / 1000000L;
|
|
}
|
|
|
|
/* ── Test 1: single-node shortcircuit returns in < 5ms ──────────────────── */
|
|
|
|
static void test_single_node_returns_quickly(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
ha.phase2_done = 0; /* first boot */
|
|
|
|
/* n_peers=1: shortcircuit, no iomgr or delaymgr needed */
|
|
struct timespec t0;
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
|
|
ha_start_election_async(&ha, NULL, 1, NULL, 100ULL, 50ULL, "ctrl-a");
|
|
|
|
long ms = elapsed_ms(&t0);
|
|
TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(5, (int)ms,
|
|
"ha_start_election_async(n_peers=1) must return in < 5ms");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_single_node_returns_quickly: PASS (elapsed=%ldms)\n", ms);
|
|
}
|
|
|
|
/* ── Test 2: single-node shortcircuit stores n_peers in ha->n_peers ──────── */
|
|
|
|
static void test_single_node_stores_n_peers(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
ha.n_peers = 0;
|
|
|
|
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.n_peers,
|
|
"ha->n_peers must be set to n_peers argument (1 for single-node)");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_single_node_stores_n_peers: PASS\n");
|
|
}
|
|
|
|
/* ── Test 3: single-node shortcircuit enters CTRL_PRIMARY_COMMITTING ──────── */
|
|
|
|
static void test_single_node_shortcircuit_evaluates_immediately(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
ha.phase2_done = 0;
|
|
/* Start in CTRL_ELECTING state so role transition is visible */
|
|
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
|
|
|
|
/* n_peers=1: must call ha_elect_evaluate_and_signal immediately.
|
|
* Fix 1 (CRITICAL): single-node shortcircuit must enter CTRL_PRIMARY_COMMITTING,
|
|
* not CTRL_PRIMARY. Task 4.4 committing timeout will promote to CTRL_PRIMARY
|
|
* once the committing phase completes (Req 8.4 / degraded-single-node path). */
|
|
ha_start_election_async(&ha, NULL, 1, NULL, 100ULL, 50ULL, "ctrl-a");
|
|
|
|
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, (int)role,
|
|
"single-node shortcircuit must enter CTRL_PRIMARY_COMMITTING (not CTRL_PRIMARY)");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_single_node_shortcircuit_evaluates_immediately: PASS (role=%d == CTRL_PRIMARY_COMMITTING)\n",
|
|
(int)role);
|
|
}
|
|
|
|
/* ── Test 6: single-node shortcircuit does NOT go directly to CTRL_PRIMARY ── */
|
|
|
|
static void test_single_node_not_primary_directly(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
|
|
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
|
|
|
|
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire);
|
|
TEST_ASSERT_NOT_EQUAL_MESSAGE((int)CTRL_PRIMARY, (int)role,
|
|
"single-node shortcircuit must NOT jump directly to CTRL_PRIMARY; "
|
|
"must go through CTRL_PRIMARY_COMMITTING first");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_single_node_not_primary_directly: PASS (role=%d)\n", (int)role);
|
|
}
|
|
|
|
/* ── Test 4: ha->n_peers set for multi-peer count too ───────────────────── */
|
|
|
|
static void test_n_peers_stored_for_multi_peer(void)
|
|
{
|
|
/* We can't test actual multi-peer endpoint setup without iomgr,
|
|
* but we can verify n_peers is stored correctly for any value.
|
|
* Pass peers=NULL to avoid actual connect attempts; the function
|
|
* should skip peer on connect failure (design spec). */
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
ha.n_peers = 0;
|
|
|
|
/* Passing n_peers=3 with peers=NULL: each peer would fail connect,
|
|
* but n_peers must be stored before attempting connects. */
|
|
ha_start_election_async(&ha, NULL, 3, NULL, 100ULL, 50ULL, "ctrl-a");
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(3, ha.n_peers,
|
|
"ha->n_peers must be set to n_peers argument even for multi-peer");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_n_peers_stored_for_multi_peer: PASS\n");
|
|
}
|
|
|
|
/* ── Test 5: single-node election increments term ───────────────────────── */
|
|
|
|
static void test_single_node_increments_term(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_async_cfg, 0);
|
|
uint64_t initial_term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
|
|
|
ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a");
|
|
|
|
uint64_t final_term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
|
TEST_ASSERT_GREATER_THAN_UINT64_MESSAGE(initial_term, final_term,
|
|
"single-node election must increment ha->term");
|
|
|
|
ha_fini(&ha);
|
|
printf("test_single_node_increments_term: PASS (term: %"PRIu64" → %"PRIu64")\n",
|
|
initial_term, final_term);
|
|
}
|
|
|
|
/* ── Suite entry point ───────────────────────────────────────────────────── */
|
|
|
|
void test_ha_start_election_async(void)
|
|
{
|
|
RUN_TEST(test_single_node_returns_quickly);
|
|
RUN_TEST(test_single_node_stores_n_peers);
|
|
RUN_TEST(test_single_node_shortcircuit_evaluates_immediately);
|
|
RUN_TEST(test_single_node_not_primary_directly);
|
|
RUN_TEST(test_n_peers_stored_for_multi_peer);
|
|
RUN_TEST(test_single_node_increments_term);
|
|
}
|