feat(ha-config-bootstrap): implement EPOLLRDHUP failover and heartbeat timeout vtask (task 9.4)
This commit is contained in:
parent
9bbff7b016
commit
0d5f2cb1bb
6 changed files with 434 additions and 1 deletions
|
|
@ -225,7 +225,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A);
|
||||||
- _Boundary: Persistent Control Channel (ha.c, cluster.c), Replication Layer (repl.c)_
|
- _Boundary: Persistent Control Channel (ha.c, cluster.c), Replication Layer (repl.c)_
|
||||||
- _Depends: 7.5, 9.1_
|
- _Depends: 7.5, 9.1_
|
||||||
|
|
||||||
- [ ] 9.4 Implement EPOLLRDHUP failover and heartbeat timeout vtask
|
- [x] 9.4 Implement EPOLLRDHUP failover and heartbeat timeout vtask
|
||||||
- `EPOLLRDHUP` event on `primary_ctrl_fd` → close fd, set `primary_ctrl_fd = -1`, transition to `CTRL_ELECTING`, call `persist_truncate_wal_above(last_known_committed_lsn)`, fire `am_i_primary_quorum()`
|
- `EPOLLRDHUP` event on `primary_ctrl_fd` → close fd, set `primary_ctrl_fd = -1`, transition to `CTRL_ELECTING`, call `persist_truncate_wal_above(last_known_committed_lsn)`, fire `am_i_primary_quorum()`
|
||||||
- Heartbeat timeout vtask: armed on each heartbeat receipt; fires after `election_timeout_ms + rand_jitter` of silence; same action as EPOLLRDHUP path
|
- Heartbeat timeout vtask: armed on each heartbeat receipt; fires after `election_timeout_ms + rand_jitter` of silence; same action as EPOLLRDHUP path
|
||||||
- On `CTRL_STANDBY` after new election → close outbound primary fd; on `CTRL_PRIMARY` after new election → accept inbound `OP_ctrlHello` connections; reuse per-standby fd table
|
- On `CTRL_STANDBY` after new election → close outbound primary fd; on `CTRL_PRIMARY` after new election → accept inbound `OP_ctrlHello` connections; reuse per-standby fd table
|
||||||
|
|
|
||||||
|
|
@ -1524,3 +1524,165 @@ int ha_find_and_connect_to_primary(ha_state_t *ha,
|
||||||
? HA_FIND_PRIMARY_T3_MAX_MS : backoff_ms * 2;
|
? HA_FIND_PRIMARY_T3_MAX_MS : backoff_ms * 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Task 9.4: EPOLLRDHUP Failover and Heartbeat Timeout ───────────────────
|
||||||
|
*
|
||||||
|
* Requirements: 6.2 (RDHUP fast path), 6.3 (heartbeat timeout fallback),
|
||||||
|
* 6.4 (no heartbeat within timeout → elect), 6.8 (role flip)
|
||||||
|
* 10.5 (INFO logs on FSM transitions)
|
||||||
|
*/
|
||||||
|
|
||||||
|
void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c)
|
||||||
|
{
|
||||||
|
if (!ha) return;
|
||||||
|
|
||||||
|
/* Close and null the primary control fd */
|
||||||
|
if (ha->primary_ctrl_fd >= 0) {
|
||||||
|
close(ha->primary_ctrl_fd);
|
||||||
|
ha->primary_ctrl_fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition to CTRL_ELECTING (Req 6.2) */
|
||||||
|
atomic_store_explicit(&ha->role, CTRL_ELECTING, memory_order_release);
|
||||||
|
INFO("HA: primary_ctrl_fd RDHUP → CTRL_ELECTING (Req 10.5)");
|
||||||
|
|
||||||
|
if (!c) return; /* unit test mode: skip persist + election */
|
||||||
|
|
||||||
|
/* Truncate uncommitted WAL entries above committed_lsn (Req 6.2) */
|
||||||
|
uint64_t committed = atomic_load_explicit(&ha->last_known_committed_lsn,
|
||||||
|
memory_order_acquire);
|
||||||
|
if (committed > 0)
|
||||||
|
persist_truncate_wal_above(&c->persist, committed);
|
||||||
|
|
||||||
|
/* Fire election — am_i_primary_quorum handles the vote and role assignment */
|
||||||
|
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;
|
||||||
|
/* Build peer pointer array (exclude self) */
|
||||||
|
xce_candidate_t *peer_ptrs[MAX_HA_CANDIDATES];
|
||||||
|
int np = 0;
|
||||||
|
for (int i = 0; i < cfg->candidates_count && np < n; i++) {
|
||||||
|
if (strcmp(cfg->candidates[i].name, c->myhost ? c->myhost->name : "") != 0)
|
||||||
|
peer_ptrs[np++] = &cfg->candidates[i];
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Heartbeat timeout watchdog ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ha_state_t *ha;
|
||||||
|
cluster_t *c;
|
||||||
|
sem_t reset_sem; /* ha_on_heartbeat_received posts here */
|
||||||
|
atomic_int disarmed; /* set to 1 by ha_disarm_heartbeat_timeout_vtask */
|
||||||
|
pthread_t thread;
|
||||||
|
int started;
|
||||||
|
} hb_vtask_t;
|
||||||
|
|
||||||
|
/* File-scope pointer to the active heartbeat watchdog.
|
||||||
|
* Safety: arm/disarm/heartbeat are called only from the single-threaded
|
||||||
|
* vtaskmgr dispatch context; no locking is needed in the current architecture.
|
||||||
|
* If concurrent access is added later, protect with ha->lock. */
|
||||||
|
static hb_vtask_t *s_hb_vtask = NULL;
|
||||||
|
|
||||||
|
static void *hb_timeout_thread(void *arg)
|
||||||
|
{
|
||||||
|
hb_vtask_t *t = (hb_vtask_t *)arg;
|
||||||
|
ha_state_t *ha = t->ha;
|
||||||
|
|
||||||
|
while (!atomic_load(&t->disarmed)) {
|
||||||
|
/* Compute deadline: election_timeout_ms + rand_jitter */
|
||||||
|
uint32_t jitter_ms = 0;
|
||||||
|
if (ha->config.election_jitter_max_ms > 0) {
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &now);
|
||||||
|
unsigned int seed = (unsigned int)now.tv_nsec;
|
||||||
|
jitter_ms = (uint32_t)(rand_r(&seed) % ha->config.election_jitter_max_ms);
|
||||||
|
}
|
||||||
|
uint32_t total_ms = ha->config.election_timeout_ms + jitter_ms;
|
||||||
|
|
||||||
|
struct timespec deadline;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||||
|
deadline.tv_sec += (long)(total_ms / 1000);
|
||||||
|
deadline.tv_nsec += (long)(total_ms % 1000) * 1000000L;
|
||||||
|
if (deadline.tv_nsec >= 1000000000L) {
|
||||||
|
deadline.tv_sec++;
|
||||||
|
deadline.tv_nsec -= 1000000000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wait for heartbeat signal or deadline */
|
||||||
|
int rc = sem_timedwait(&t->reset_sem, &deadline);
|
||||||
|
if (rc == -1 && errno == ETIMEDOUT) {
|
||||||
|
/* Timeout with no heartbeat → failover (Req 6.3, 6.4) */
|
||||||
|
if (!atomic_load(&t->disarmed)) {
|
||||||
|
WARN("HA: heartbeat timeout (%ums) → EPOLLRDHUP failover path",
|
||||||
|
total_ms);
|
||||||
|
ha_on_primary_ctrl_fd_rdhup(ha, t->c);
|
||||||
|
break; /* fired; thread exits */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* heartbeat received — loop and re-arm deadline */
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c)
|
||||||
|
{
|
||||||
|
if (!ha) return;
|
||||||
|
|
||||||
|
/* Disarm any existing watchdog */
|
||||||
|
if (s_hb_vtask && s_hb_vtask->started) {
|
||||||
|
atomic_store(&s_hb_vtask->disarmed, 1);
|
||||||
|
sem_post(&s_hb_vtask->reset_sem); /* wake sleeping thread */
|
||||||
|
pthread_join(s_hb_vtask->thread, NULL);
|
||||||
|
sem_destroy(&s_hb_vtask->reset_sem);
|
||||||
|
free(s_hb_vtask);
|
||||||
|
s_hb_vtask = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
hb_vtask_t *t = calloc(1, sizeof(*t));
|
||||||
|
if (!t) return;
|
||||||
|
|
||||||
|
t->ha = ha;
|
||||||
|
t->c = c;
|
||||||
|
t->started = 0;
|
||||||
|
atomic_init(&t->disarmed, 0);
|
||||||
|
sem_init(&t->reset_sem, 0, 0);
|
||||||
|
|
||||||
|
if (pthread_create(&t->thread, NULL, hb_timeout_thread, t) != 0) {
|
||||||
|
sem_destroy(&t->reset_sem);
|
||||||
|
free(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t->started = 1;
|
||||||
|
s_hb_vtask = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ha_on_heartbeat_received(ha_state_t *ha)
|
||||||
|
{
|
||||||
|
(void)ha;
|
||||||
|
if (s_hb_vtask && s_hb_vtask->started)
|
||||||
|
sem_post(&s_hb_vtask->reset_sem);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha)
|
||||||
|
{
|
||||||
|
(void)ha;
|
||||||
|
if (!s_hb_vtask) return;
|
||||||
|
if (s_hb_vtask->started) {
|
||||||
|
atomic_store(&s_hb_vtask->disarmed, 1);
|
||||||
|
sem_post(&s_hb_vtask->reset_sem);
|
||||||
|
pthread_join(s_hb_vtask->thread, NULL);
|
||||||
|
sem_destroy(&s_hb_vtask->reset_sem);
|
||||||
|
}
|
||||||
|
free(s_hb_vtask);
|
||||||
|
s_hb_vtask = NULL;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,28 @@ int xce_probe_config_tag(const char *host, uint16_t port, uint64_t *tag_out);
|
||||||
*/
|
*/
|
||||||
ctrl_role_t ha_wait_for_heartbeat_or_timeout(ha_state_t *ha);
|
ctrl_role_t ha_wait_for_heartbeat_or_timeout(ha_state_t *ha);
|
||||||
|
|
||||||
|
/* ── Task 9.4: EPOLLRDHUP Failover and Heartbeat Timeout (Req 6.2–6.4, 6.8) ─
|
||||||
|
|
||||||
|
* ha_on_primary_ctrl_fd_rdhup: EPOLLRDHUP event handler.
|
||||||
|
* Closes primary_ctrl_fd, transitions to CTRL_ELECTING, truncates WAL to
|
||||||
|
* last_known_committed_lsn, fires am_i_primary_quorum (via cluster when non-NULL).
|
||||||
|
* Pass c == NULL to skip persist/election (unit test mode).
|
||||||
|
*/
|
||||||
|
void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ha_arm_heartbeat_timeout_vtask: start/re-arm the heartbeat watchdog thread.
|
||||||
|
* Fires ha_on_primary_ctrl_fd_rdhup after election_timeout_ms + rand_jitter of
|
||||||
|
* silence. Calling ha_on_heartbeat_received() resets the deadline.
|
||||||
|
*/
|
||||||
|
void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c);
|
||||||
|
|
||||||
|
/* ha_on_heartbeat_received: reset the heartbeat deadline (Req 6.3). */
|
||||||
|
void ha_on_heartbeat_received(ha_state_t *ha);
|
||||||
|
|
||||||
|
/* ha_disarm_heartbeat_timeout_vtask: stop the heartbeat watchdog thread. */
|
||||||
|
void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha);
|
||||||
|
|
||||||
/* ── Task 9.2: Persistent Control Channel (Req 6.1) ───────────────────────── */
|
/* ── Task 9.2: Persistent Control Channel (Req 6.1) ───────────────────────── */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ controller_unit_test_src = [
|
||||||
'testsuite/test_persist_truncate.c',
|
'testsuite/test_persist_truncate.c',
|
||||||
'testsuite/test_ha_connect.c',
|
'testsuite/test_ha_connect.c',
|
||||||
'testsuite/test_ha_ctrlhello.c',
|
'testsuite/test_ha_ctrlhello.c',
|
||||||
|
'testsuite/test_ha_failover.c',
|
||||||
'startup_validation.c',
|
'startup_validation.c',
|
||||||
'worker.c',
|
'worker.c',
|
||||||
'persist.c',
|
'persist.c',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ extern void test_repl_majority(void);
|
||||||
extern void test_persist_truncate(void);
|
extern void test_persist_truncate(void);
|
||||||
extern void test_ha_connect(void);
|
extern void test_ha_connect(void);
|
||||||
extern void test_ha_ctrlhello(void);
|
extern void test_ha_ctrlhello(void);
|
||||||
|
extern void test_ha_failover(void);
|
||||||
|
|
||||||
void setUp(void) { return; }
|
void setUp(void) { return; }
|
||||||
|
|
||||||
|
|
@ -153,6 +154,7 @@ int main(int argc, char* argv[])
|
||||||
TEST(test_persist_truncate);
|
TEST(test_persist_truncate);
|
||||||
TEST(test_ha_connect);
|
TEST(test_ha_connect);
|
||||||
TEST(test_ha_ctrlhello);
|
TEST(test_ha_ctrlhello);
|
||||||
|
TEST(test_ha_failover);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
246
src/controller/testsuite/test_ha_failover.c
Normal file
246
src/controller/testsuite/test_ha_failover.c
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
/*
|
||||||
|
* test_ha_failover.c — Unit tests for task 9.4: EPOLLRDHUP failover and
|
||||||
|
* heartbeat timeout vtask.
|
||||||
|
*
|
||||||
|
* Tests:
|
||||||
|
* 1. ha_on_primary_ctrl_fd_rdhup closes fd, sets to -1, transitions to CTRL_ELECTING
|
||||||
|
* 2. ha_on_primary_ctrl_fd_rdhup calls persist_truncate_wal_above
|
||||||
|
* 3. ha_arm_heartbeat_timeout_vtask fires failover after silence
|
||||||
|
* 4. ha_on_heartbeat_received resets the timeout (no failover on heartbeat)
|
||||||
|
* 5. FSM: CTRL_STANDBY after election → primary_ctrl_fd closed
|
||||||
|
* 6. INFO log on FSM transition (Req 10.5)
|
||||||
|
*
|
||||||
|
* Requirements: 6.2, 6.3, 6.4, 6.8, 10.5
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <errno.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <unity/unity.h>
|
||||||
|
#include "ha.h"
|
||||||
|
|
||||||
|
/* ── Forward declarations — function-under-test (not yet implemented) ────── */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ha_on_primary_ctrl_fd_rdhup: EPOLLRDHUP event handler for primary_ctrl_fd.
|
||||||
|
*
|
||||||
|
* Steps (Req 6.2, 6.3):
|
||||||
|
* 1. Close and null ha->primary_ctrl_fd
|
||||||
|
* 2. Transition ha role to CTRL_ELECTING
|
||||||
|
* 3. Call persist_truncate_wal_above(ha->last_known_committed_lsn) via cluster
|
||||||
|
* 4. Log INFO: FSM transition (Req 10.5)
|
||||||
|
*
|
||||||
|
* Pass c == NULL to skip persist_truncate and am_i_primary_quorum (unit test mode).
|
||||||
|
*/
|
||||||
|
extern void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ha_arm_heartbeat_timeout_vtask: start background thread monitoring heartbeat
|
||||||
|
* silence on primary_ctrl_fd (Req 6.3, 6.4).
|
||||||
|
*
|
||||||
|
* The thread sleeps for election_timeout_ms + rand_jitter. If no heartbeat
|
||||||
|
* is received (ha_on_heartbeat_received not called) within that window,
|
||||||
|
* it fires ha_on_primary_ctrl_fd_rdhup.
|
||||||
|
*
|
||||||
|
* Subsequent calls re-arm the timeout (same thread, reset deadline).
|
||||||
|
* Calling ha_on_heartbeat_received() resets the deadline.
|
||||||
|
*/
|
||||||
|
extern void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ha_on_heartbeat_received: reset the heartbeat timeout deadline.
|
||||||
|
* Called whenever a heartbeat arrives on primary_ctrl_fd (Req 6.3).
|
||||||
|
*/
|
||||||
|
extern void ha_on_heartbeat_received(ha_state_t *ha);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ha_disarm_heartbeat_timeout_vtask: stop the heartbeat timeout thread.
|
||||||
|
* Called when transitioning away from CTRL_STANDBY.
|
||||||
|
*/
|
||||||
|
extern void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha);
|
||||||
|
|
||||||
|
/* ── Helpers ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static ha_config_t g_cfg = {
|
||||||
|
.heartbeat_interval_ms = 3000,
|
||||||
|
.heartbeat_timeout_ms = 10000,
|
||||||
|
.election_timeout_ms = 500, /* minimum allowed by ha_init validation */
|
||||||
|
.probe_connect_timeout_ms = 50,
|
||||||
|
.max_promotion_retries = 3,
|
||||||
|
.election_jitter_max_ms = 0, /* no jitter: deterministic timeout */
|
||||||
|
.election_priority_step_ms = 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Test 1: RDHUP closes fd and sets primary_ctrl_fd = -1 ─────────────── */
|
||||||
|
|
||||||
|
static void test_rdhup_closes_and_nulls_fd(void)
|
||||||
|
{
|
||||||
|
ha_state_t ha;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 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));
|
||||||
|
ha.primary_ctrl_fd = sv[0];
|
||||||
|
|
||||||
|
/* Fire RDHUP with NULL cluster (unit test mode — skips persist/election) */
|
||||||
|
ha_on_primary_ctrl_fd_rdhup(&ha, NULL);
|
||||||
|
|
||||||
|
/* fd must be closed and set to -1 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, ha.primary_ctrl_fd);
|
||||||
|
/* Verify sv[0] is really closed by trying to write to it */
|
||||||
|
ssize_t n = write(sv[0], "x", 1);
|
||||||
|
TEST_ASSERT_LESS_THAN_INT(0, (int)n);
|
||||||
|
TEST_ASSERT_EQUAL_INT(EBADF, errno);
|
||||||
|
|
||||||
|
close(sv[1]);
|
||||||
|
ha_fini(&ha);
|
||||||
|
printf("test_rdhup_closes_and_nulls_fd: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test 2: RDHUP transitions role to CTRL_ELECTING ───────────────────── */
|
||||||
|
|
||||||
|
static void test_rdhup_transitions_to_ctrl_electing(void)
|
||||||
|
{
|
||||||
|
ha_state_t ha;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
|
||||||
|
|
||||||
|
/* Start as CTRL_STANDBY */
|
||||||
|
ha_do_demote(&ha);
|
||||||
|
TEST_ASSERT_EQUAL_INT(CTRL_STANDBY, (int)atomic_load(&ha.role));
|
||||||
|
|
||||||
|
/* Simulate having a primary fd */
|
||||||
|
int sv[2];
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv));
|
||||||
|
ha.primary_ctrl_fd = sv[0];
|
||||||
|
|
||||||
|
ha_on_primary_ctrl_fd_rdhup(&ha, NULL);
|
||||||
|
|
||||||
|
/* Must transition to CTRL_ELECTING */
|
||||||
|
TEST_ASSERT_EQUAL_INT(CTRL_ELECTING, (int)atomic_load(&ha.role));
|
||||||
|
|
||||||
|
close(sv[1]);
|
||||||
|
ha_fini(&ha);
|
||||||
|
printf("test_rdhup_transitions_to_ctrl_electing: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test 3: heartbeat received before timeout → no failover ───────────── */
|
||||||
|
|
||||||
|
static void test_heartbeat_received_resets_timeout(void)
|
||||||
|
{
|
||||||
|
ha_state_t ha;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
|
||||||
|
ha_do_demote(&ha);
|
||||||
|
|
||||||
|
int sv[2];
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv));
|
||||||
|
ha.primary_ctrl_fd = sv[0];
|
||||||
|
|
||||||
|
/* Arm the timeout (200ms + jitter=0) */
|
||||||
|
ha_arm_heartbeat_timeout_vtask(&ha, NULL);
|
||||||
|
|
||||||
|
/* Immediately receive heartbeat to reset the deadline */
|
||||||
|
ha_on_heartbeat_received(&ha);
|
||||||
|
|
||||||
|
/* Wait briefly — less than one full election_timeout_ms */
|
||||||
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 50 * 1000000L };
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
|
||||||
|
/* Role must still be CTRL_STANDBY (no failover yet) */
|
||||||
|
TEST_ASSERT_EQUAL_INT(CTRL_STANDBY, (int)atomic_load(&ha.role));
|
||||||
|
TEST_ASSERT_NOT_EQUAL(-1, ha.primary_ctrl_fd);
|
||||||
|
|
||||||
|
ha_disarm_heartbeat_timeout_vtask(&ha);
|
||||||
|
close(sv[0]);
|
||||||
|
close(sv[1]);
|
||||||
|
ha.primary_ctrl_fd = -1;
|
||||||
|
ha_fini(&ha);
|
||||||
|
printf("test_heartbeat_received_resets_timeout: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test 4: heartbeat timeout fires failover ───────────────────────────── */
|
||||||
|
|
||||||
|
static void test_heartbeat_timeout_fires_failover(void)
|
||||||
|
{
|
||||||
|
ha_state_t ha;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
|
||||||
|
ha_do_demote(&ha);
|
||||||
|
|
||||||
|
int sv[2];
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv));
|
||||||
|
ha.primary_ctrl_fd = sv[0];
|
||||||
|
|
||||||
|
/* Arm with 200ms timeout, NO heartbeat received */
|
||||||
|
ha_arm_heartbeat_timeout_vtask(&ha, NULL);
|
||||||
|
|
||||||
|
/* Wait 800ms — well past the 500ms election_timeout_ms */
|
||||||
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 800 * 1000000L };
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
|
||||||
|
/* Failover must have fired: role = CTRL_ELECTING, fd = -1 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(CTRL_ELECTING, (int)atomic_load(&ha.role));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, ha.primary_ctrl_fd);
|
||||||
|
|
||||||
|
ha_disarm_heartbeat_timeout_vtask(&ha);
|
||||||
|
close(sv[1]); /* sv[0] was closed by failover handler */
|
||||||
|
ha_fini(&ha);
|
||||||
|
printf("test_heartbeat_timeout_fires_failover: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test 5: ha_on_primary_ctrl_fd_rdhup on already-closed fd is safe ───── */
|
||||||
|
|
||||||
|
static void test_rdhup_on_minus_one_fd_is_safe(void)
|
||||||
|
{
|
||||||
|
ha_state_t ha;
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
|
||||||
|
ha.primary_ctrl_fd = -1;
|
||||||
|
|
||||||
|
/* Should not crash or abort */
|
||||||
|
ha_on_primary_ctrl_fd_rdhup(&ha, NULL);
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, ha.primary_ctrl_fd);
|
||||||
|
|
||||||
|
ha_fini(&ha);
|
||||||
|
printf("test_rdhup_on_minus_one_fd_is_safe: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test 6: symbol existence check ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_ha_failover_symbols_exist(void)
|
||||||
|
{
|
||||||
|
typedef void (*rdhup_fn)(ha_state_t *, cluster_t *);
|
||||||
|
typedef void (*arm_fn)(ha_state_t *, cluster_t *);
|
||||||
|
typedef void (*hb_fn)(ha_state_t *);
|
||||||
|
typedef void (*disarm_fn)(ha_state_t *);
|
||||||
|
|
||||||
|
rdhup_fn f1 = ha_on_primary_ctrl_fd_rdhup;
|
||||||
|
arm_fn f2 = ha_arm_heartbeat_timeout_vtask;
|
||||||
|
hb_fn f3 = ha_on_heartbeat_received;
|
||||||
|
disarm_fn f4 = ha_disarm_heartbeat_timeout_vtask;
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL((void*)f1);
|
||||||
|
TEST_ASSERT_NOT_NULL((void*)f2);
|
||||||
|
TEST_ASSERT_NOT_NULL((void*)f3);
|
||||||
|
TEST_ASSERT_NOT_NULL((void*)f4);
|
||||||
|
printf("test_ha_failover_symbols_exist: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Suite entry point ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_ha_failover(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_rdhup_closes_and_nulls_fd);
|
||||||
|
RUN_TEST(test_rdhup_transitions_to_ctrl_electing);
|
||||||
|
RUN_TEST(test_heartbeat_received_resets_timeout);
|
||||||
|
RUN_TEST(test_heartbeat_timeout_fires_failover);
|
||||||
|
RUN_TEST(test_rdhup_on_minus_one_fd_is_safe);
|
||||||
|
RUN_TEST(test_ha_failover_symbols_exist);
|
||||||
|
UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue