From a289edb77736d475db35ce9b7686e837852db63d Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 7 May 2026 14:08:15 +0000 Subject: [PATCH] feat: ha_state_t, ha_config_t, ha_init/fini, role enum + config validation (P1-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/controller/ha.c | 75 ++++++++++++++++- src/controller/ha.h | 84 ++++++++++++++++++- src/controller/meson.build | 2 + src/controller/testsuite/controller_test.c | 2 + src/controller/testsuite/test_ha_role.c | 94 ++++++++++++++++++++++ 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/controller/testsuite/test_ha_role.c diff --git a/src/controller/ha.c b/src/controller/ha.c index 4d2f3a1a..5d0644bb 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -1,3 +1,76 @@ +#define _GNU_SOURCE #include "ha.h" -/* P1-2: implementation added in next task */ +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/src/controller/ha.h b/src/controller/ha.h index f1ee670a..1abe7680 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -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 +#include +#include +#include + +#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 */ diff --git a/src/controller/meson.build b/src/controller/meson.build index 53e70f6c..f8b52123 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -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 = [ diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index 9283c0f4..1904b622 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -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(); } diff --git a/src/controller/testsuite/test_ha_role.c b/src/controller/testsuite/test_ha_role.c new file mode 100644 index 00000000..5c56bd67 --- /dev/null +++ b/src/controller/testsuite/test_ha_role.c @@ -0,0 +1,94 @@ +#include +#include +#include + +#include + +#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); +}