diff --git a/src/controller/ha.c b/src/controller/ha.c index 928b31a5..879a407e 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,62 @@ int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority) return 0; } +/* ── P1-3a: promotion / demotion / SIGUSR1 ─────────────────── */ + +/* + * Async-signal-safe: the handler only performs an atomic store on an + * int-width type, which is async-signal-safe on all POSIX platforms + * (POSIX.1-2017 §2.4.3). The pointer is set once by ha_install_sigusr1() + * before signals are enabled; no locking is required. + */ +static ha_state_t *g_sigusr1_ha = NULL; + +static void sigusr1_handler(int sig) +{ + (void)sig; + if (g_sigusr1_ha) + atomic_store_explicit(&g_sigusr1_ha->failover_requested, 1, + memory_order_release); +} + +void ha_install_sigusr1(ha_state_t *ha) +{ + g_sigusr1_ha = ha; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigusr1_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGUSR1, &sa, NULL); +} + +void ha_do_promote(ha_state_t *ha) +{ + /* Step 1: Increment election term — monotonically increasing fencing token */ + atomic_fetch_add_explicit(&ha->term, 1, memory_order_acq_rel); + + /* Step 2: Set role to PRIMARY */ + atomic_store_explicit(&ha->role, (int32_t)CTRL_PRIMARY, memory_order_release); + + /* Step 3: Reset last_received_crc chain (COH-5 — new primary starts fresh chain) */ + ha->last_received_crc = WAL_CRC_NONE; + + /* Steps 4+: hook re-registration, standby heartbeat, cluster_start() — + * deferred to ha_promote_to_primary(cluster_t*) in P1-5. */ +} + +void ha_do_demote(ha_state_t *ha) +{ + /* Step 1: Set role to STANDBY — vcpu threads will reject new primary ops */ + atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release); + + /* Step 2: Reset hook idempotency guard — next promote will re-register */ + ha->hooks_registered = 0; + + /* Step 3: hook disable done in ha_demote_to_standby(cluster_t*) in P1-5 + * because it needs the cluster's hooksys_t. */ +} + /* ── Watchdog thread implementation ────────────────────────── */ void ha_check_heartbeat_nocluster(ha_state_t *ha) diff --git a/src/controller/ha.h b/src/controller/ha.h index 43bf6fd8..70468684 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -97,6 +97,29 @@ int ha_promote_to_primary(cluster_t *c); */ int ha_demote_to_standby(cluster_t *c); +/* ── Internal promotion/demotion (P1-3a) ───────────────────── */ + +/* + * ha_do_promote: increment term, set role=PRIMARY, reset last_received_crc. + * No cluster_t dependency — fully testable standalone. + * Callers: ha_promote_to_primary() (P1-5), watchdog failover path. + */ +void ha_do_promote(ha_state_t *ha); + +/* + * ha_do_demote: set role=STANDBY, reset hooks_registered idempotency guard. + * Hook disable requires cluster_t and is done in ha_demote_to_standby() (P1-5). + */ +void ha_do_demote(ha_state_t *ha); + +/* + * ha_install_sigusr1: register a SIGUSR1 handler that atomically sets + * ha->failover_requested. The handler uses only async-signal-safe operations + * (atomic store on an int-width type — POSIX.1-2017 §2.4.3). + * Must be called once, before any signal can arrive. + */ +void ha_install_sigusr1(ha_state_t *ha); + /* ── Watchdog thread API (P1-3) ────────────────────────────── */ /* diff --git a/src/controller/testsuite/test_ha_role.c b/src/controller/testsuite/test_ha_role.c index c2436c55..7a05d6e5 100644 --- a/src/controller/testsuite/test_ha_role.c +++ b/src/controller/testsuite/test_ha_role.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -151,6 +152,86 @@ static void test_heartbeat_stale_no_crash_without_primary(void) printf("test_heartbeat_stale_no_crash_without_primary: PASS\n"); } +/* ── P1-3a tests ─────────────────────────────────────────────── */ + +/* Term must increment on each promotion attempt */ +static void test_promote_increments_term(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); + uint64_t term_before = atomic_load_explicit(&ha.term, memory_order_acquire); + + ha_do_promote(&ha); /* internal promotion logic — no cluster_t needed */ + + uint64_t term_after = atomic_load_explicit(&ha.term, memory_order_acquire); + TEST_ASSERT_TRUE(term_after > term_before); + TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY, + (int)atomic_load_explicit(&ha.role, memory_order_acquire)); + + ha_fini(&ha); + printf("test_promote_increments_term: PASS\n"); +} + +/* Demotion must set role back to STANDBY */ +static void test_demote_sets_standby_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); + ha_do_promote(&ha); + TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY, + (int)atomic_load_explicit(&ha.role, memory_order_acquire)); + + ha_do_demote(&ha); + TEST_ASSERT_EQUAL_INT(CTRL_STANDBY, + (int)atomic_load_explicit(&ha.role, memory_order_acquire)); + + ha_fini(&ha); + printf("test_demote_sets_standby_role: PASS\n"); +} + +/* SIGUSR1 must set failover_requested via async-signal-safe atomic store */ +static void test_sigusr1_sets_failover_requested(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); + + /* Install real SIGUSR1 handler that uses only the atomic flag */ + ha_install_sigusr1(&ha); + + /* Send SIGUSR1 to self */ + raise(SIGUSR1); + + /* Handler must have set failover_requested */ + TEST_ASSERT_TRUE( + atomic_load_explicit(&ha.failover_requested, memory_order_acquire) != 0); + + /* Restore default SIGUSR1 handler */ + signal(SIGUSR1, SIG_DFL); + ha_fini(&ha); + printf("test_sigusr1_sets_failover_requested: PASS\n"); +} + void test_ha_role(void) { RUN_TEST(test_ha_init_default_role_is_standby); @@ -161,4 +242,8 @@ void test_ha_role(void) RUN_TEST(test_watchdog_starts_and_stops); RUN_TEST(test_heartbeat_no_action_when_no_primary); RUN_TEST(test_heartbeat_stale_no_crash_without_primary); + /* P1-3a */ + RUN_TEST(test_promote_increments_term); + RUN_TEST(test_demote_sets_standby_role); + RUN_TEST(test_sigusr1_sets_failover_requested); }