feat: ha_state_t, ha_config_t, ha_init/fini, role enum + config validation (P1-2)
- Full ha.h: ctrl_role_t enum (CTRL_PRIMARY/CTRL_STANDBY), ha_config_t, ha_state_t with _Atomic fields (term, role, applied_lsn, promotion counters, watchdog_stop), pthread_mutex_t lock, and public API. - endpoint_t* omitted from ha_state_t — including netio.h would pull OpenSSL headers and risk circular deps; watchdog (P1-3) includes netio.h directly where needed. - ha_init validates: heartbeat_interval_ms >= 2000, heartbeat_timeout_ms >= heartbeat_interval_ms, election_timeout_ms >= 500; returns -EINVAL on bad config; zeros struct and sets initial role to CTRL_STANDBY. - ha_fini destroys mutex; watchdog thread join deferred to P1-3. - am_i_primary / ha_promote_to_primary / ha_demote_to_standby are no-op stubs; cluster_t->ha field not yet wired (P1-5), so these avoid incomplete-type dereferences until then. - TDD: 5 new tests in test_ha_role.c confirmed RED before implementation, all 5 GREEN after; all prior controller tests remain green.
This commit is contained in:
parent
f155b1ef97
commit
a289edb777
5 changed files with 255 additions and 2 deletions
|
|
@ -1,3 +1,76 @@
|
|||
#define _GNU_SOURCE
|
||||
#include "ha.h"
|
||||
|
||||
/* P1-2: implementation added in next task */
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority)
|
||||
{
|
||||
if (!ha || !cfg)
|
||||
return -EINVAL;
|
||||
|
||||
if (cfg->heartbeat_interval_ms < 2000)
|
||||
return -EINVAL;
|
||||
if (cfg->heartbeat_timeout_ms < cfg->heartbeat_interval_ms)
|
||||
return -EINVAL;
|
||||
if (cfg->election_timeout_ms < 500)
|
||||
return -EINVAL;
|
||||
|
||||
memset(ha, 0, sizeof(*ha));
|
||||
ha->config = *cfg;
|
||||
ha->my_priority = my_priority;
|
||||
|
||||
atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release);
|
||||
atomic_store_explicit(&ha->term, 0ULL, memory_order_release);
|
||||
atomic_store_explicit(&ha->applied_lsn, 0ULL, memory_order_release);
|
||||
atomic_store_explicit(&ha->promotion_failed, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&ha->promotion_retry_count, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&ha->in_catch_up, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&ha->failover_requested, 0, memory_order_relaxed);
|
||||
atomic_store_explicit(&ha->watchdog_stop, 0, memory_order_relaxed);
|
||||
|
||||
pthread_mutex_init(&ha->lock, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ha_fini(ha_state_t *ha)
|
||||
{
|
||||
if (!ha)
|
||||
return;
|
||||
/* Watchdog thread join added in P1-3 */
|
||||
pthread_mutex_destroy(&ha->lock);
|
||||
}
|
||||
|
||||
/*
|
||||
* The three cluster_t-dependent stubs below cannot dereference c->ha until
|
||||
* P1-5 adds `ha_state_t ha` to cluster_t. They compile as no-ops now and
|
||||
* will be fleshed out in P1-3a after P1-5 wires the field in.
|
||||
*/
|
||||
|
||||
int am_i_primary(cluster_t *c)
|
||||
{
|
||||
(void)c;
|
||||
/* P1-3 adds real election probing.
|
||||
* Stub: always report not-primary until cluster wiring is in place. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ha_promote_to_primary(cluster_t *c)
|
||||
{
|
||||
(void)c;
|
||||
/* Full sequence in P1-3a (WAL flush, hook calls, peer notify).
|
||||
* Stub: no-op — role management moved to P1-3a. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ha_demote_to_standby(cluster_t *c)
|
||||
{
|
||||
(void)c;
|
||||
/* Full sequence in P1-3a.
|
||||
* Stub: no-op — role management moved to P1-3a. */
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,90 @@
|
|||
* No new TLS infrastructure is needed — XCE runs on a trusted management LAN.
|
||||
* Peer identity is validated by host:port match against the --peers candidate list.
|
||||
* See design doc Part 5.9 and memory: Persistence & HA Trusted LAN Decision.
|
||||
*
|
||||
* P1-2: full struct definitions and API.
|
||||
* endpoint_t* (primary_ep) is intentionally omitted here — including netio.h
|
||||
* would pull in heavy OpenSSL headers and create circular dependency risks.
|
||||
* The watchdog (P1-3) will include netio.h directly where it needs endpoint_t.
|
||||
*/
|
||||
|
||||
/* Full struct definitions and API declarations added in P1-2. */
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "config.h" /* XCE_MAXNAME_LEN, MAX_STANDBYS, etc. */
|
||||
|
||||
/* Forward declaration — avoids circular include with cluster_def.h */
|
||||
typedef struct cluster_s cluster_t;
|
||||
|
||||
/* ── Role enum ─────────────────────────────────────────────── */
|
||||
typedef enum {
|
||||
CTRL_PRIMARY = 0,
|
||||
CTRL_STANDBY = 1
|
||||
} ctrl_role_t;
|
||||
|
||||
/* ── HA configuration ──────────────────────────────────────── */
|
||||
typedef struct {
|
||||
uint32_t heartbeat_interval_ms; /* Primary heartbeat send interval (min 2000 ms) */
|
||||
uint32_t heartbeat_timeout_ms; /* Standby timeout before promoting (>= interval) */
|
||||
uint32_t election_timeout_ms; /* Election probe shared deadline (min 500 ms) */
|
||||
uint32_t probe_connect_timeout_ms; /* Per-probe TCP connect timeout */
|
||||
uint32_t max_promotion_retries; /* Max consecutive failed promotion attempts */
|
||||
} ha_config_t;
|
||||
|
||||
/* ── HA runtime state (embedded in cluster_t as `ha_state_t ha` in P1-5) ── */
|
||||
typedef struct {
|
||||
_Atomic uint64_t term; /* Election term — fencing token */
|
||||
_Atomic int32_t role; /* Cast to ctrl_role_t at access sites */
|
||||
int my_priority; /* Index in --peers list (lower = higher priority) */
|
||||
_Atomic uint64_t applied_lsn; /* Standby: last applied WAL LSN */
|
||||
char primary_host[XCE_MAXNAME_LEN]; /* Hostname of current primary */
|
||||
uint32_t primary_port; /* Port of current primary */
|
||||
struct timespec last_heartbeat; /* CLOCK_MONOTONIC timestamp of last heartbeat */
|
||||
_Atomic int promotion_failed;
|
||||
_Atomic int promotion_retry_count;
|
||||
_Atomic int in_catch_up; /* Non-zero while processing historical WAL entries */
|
||||
_Atomic int failover_requested; /* Set by SIGUSR1 handler; watchdog executes */
|
||||
int hooks_registered; /* Idempotency guard for xce_add_hook calls */
|
||||
uint32_t last_received_crc; /* Chained CRC of last received replicated entry */
|
||||
ha_config_t config;
|
||||
pthread_mutex_t lock; /* Protects primary_host/port + last_heartbeat */
|
||||
/* Watchdog thread — started in P1-3 */
|
||||
_Atomic int watchdog_stop;
|
||||
pthread_t watchdog_thread;
|
||||
} ha_state_t;
|
||||
|
||||
/* ── Public API ────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* ha_init: zero-initialise *ha, validate cfg, store config and my_priority.
|
||||
* Returns 0 on success, -EINVAL on bad config.
|
||||
* Initial role is always CTRL_STANDBY.
|
||||
*/
|
||||
int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority);
|
||||
|
||||
/*
|
||||
* ha_fini: destroy mutex. Watchdog thread join added in P1-3.
|
||||
*/
|
||||
void ha_fini(ha_state_t *ha);
|
||||
|
||||
/*
|
||||
* am_i_primary: stub — priority-0 node is primary.
|
||||
* Full election probing added in P1-3.
|
||||
*/
|
||||
int am_i_primary(cluster_t *c);
|
||||
|
||||
/*
|
||||
* ha_promote_to_primary: stub — atomically sets role to PRIMARY.
|
||||
* Full promotion sequence (hooks, WAL flush, peer notify) added in P1-3a.
|
||||
*/
|
||||
int ha_promote_to_primary(cluster_t *c);
|
||||
|
||||
/*
|
||||
* ha_demote_to_standby: stub — atomically sets role to STANDBY.
|
||||
* Full demotion sequence added in P1-3a.
|
||||
*/
|
||||
int ha_demote_to_standby(cluster_t *c);
|
||||
|
||||
#endif /* HA_H */
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@ controller_unit_test_src = [
|
|||
'testsuite/test_e2e_hooks.c',
|
||||
'testsuite/test_e2e_gc.c',
|
||||
'testsuite/test_e2e_capnp.c',
|
||||
'testsuite/test_ha_role.c',
|
||||
'persist.c',
|
||||
'ha.c',
|
||||
]
|
||||
|
||||
controller_unit_test_deps = [
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ extern void test_e2e_checkpoint(void);
|
|||
extern void test_e2e_hooks(void);
|
||||
extern void test_e2e_gc(void);
|
||||
extern void test_e2e_capnp(void);
|
||||
extern void test_ha_role(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -107,6 +108,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_e2e_hooks);
|
||||
TEST(test_e2e_gc);
|
||||
TEST(test_e2e_capnp);
|
||||
TEST(test_ha_role);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
94
src/controller/testsuite/test_ha_role.c
Normal file
94
src/controller/testsuite/test_ha_role.c
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "ha.h"
|
||||
|
||||
static void test_ha_init_default_role_is_standby(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
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,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
TEST_ASSERT_EQUAL_INT(CTRL_STANDBY,
|
||||
(int)atomic_load_explicit(&ha.role, memory_order_acquire));
|
||||
ha_fini(&ha);
|
||||
printf("test_ha_init_default_role_is_standby: PASS\n");
|
||||
}
|
||||
|
||||
static void test_ha_init_rejects_fast_heartbeat(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
ha_config_t cfg = {
|
||||
.heartbeat_interval_ms = 1999, /* below 2000ms floor */
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
TEST_ASSERT_NOT_EQUAL(0, ha_init(&ha, &cfg, 0));
|
||||
printf("test_ha_init_rejects_fast_heartbeat: PASS\n");
|
||||
}
|
||||
|
||||
static void test_ha_init_rejects_short_election_timeout(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
ha_config_t cfg = {
|
||||
.heartbeat_interval_ms = 3000,
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 499, /* below 500ms floor */
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
TEST_ASSERT_NOT_EQUAL(0, ha_init(&ha, &cfg, 0));
|
||||
printf("test_ha_init_rejects_short_election_timeout: PASS\n");
|
||||
}
|
||||
|
||||
static void test_ha_init_rejects_timeout_less_than_interval(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
ha_config_t cfg = {
|
||||
.heartbeat_interval_ms = 5000,
|
||||
.heartbeat_timeout_ms = 4999, /* must be >= heartbeat_interval_ms */
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
TEST_ASSERT_NOT_EQUAL(0, ha_init(&ha, &cfg, 0));
|
||||
printf("test_ha_init_rejects_timeout_less_than_interval: PASS\n");
|
||||
}
|
||||
|
||||
static void test_ha_promote_sets_primary_role(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
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,
|
||||
};
|
||||
ha_init(&ha, &cfg, 0);
|
||||
/* Directly set role to PRIMARY (stub path — full promotion in P1-3a) */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY,
|
||||
(int)atomic_load_explicit(&ha.role, memory_order_acquire));
|
||||
ha_fini(&ha);
|
||||
printf("test_ha_promote_sets_primary_role: PASS\n");
|
||||
}
|
||||
|
||||
void test_ha_role(void)
|
||||
{
|
||||
RUN_TEST(test_ha_init_default_role_is_standby);
|
||||
RUN_TEST(test_ha_init_rejects_fast_heartbeat);
|
||||
RUN_TEST(test_ha_init_rejects_short_election_timeout);
|
||||
RUN_TEST(test_ha_init_rejects_timeout_less_than_interval);
|
||||
RUN_TEST(test_ha_promote_sets_primary_role);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue