feat: ha_do_promote/demote + SIGUSR1 atomic flag handler (P1-3a)

- ha_do_promote(): atomic_fetch_add term, set role=PRIMARY, reset
  last_received_crc to WAL_CRC_NONE (COH-5 chain restart on promotion)
- ha_do_demote(): set role=STANDBY, reset hooks_registered idempotency guard
- ha_install_sigusr1(): sigaction handler stores only atomic int —
  async-signal-safe per POSIX.1-2017 §2.4.3; global g_sigusr1_ha set
  once before signals are enabled
- ha_promote_to_primary() / ha_demote_to_standby() remain (void)c stubs
  pending P1-5 cluster wiring

Tests: 3 new cases in test_ha_role.c
  test_promote_increments_term: term > term_before, role == PRIMARY
  test_demote_sets_standby_role: role reverts to STANDBY after promote+demote
  test_sigusr1_sets_failover_requested: raise(SIGUSR1) sets atomic flag
Result: 100 Tests 0 Failures 0 Ignored (controller suite)
This commit is contained in:
Rongsong Shen 2026-05-07 14:40:43 +00:00
parent 2fae4c3df5
commit 652b015458
3 changed files with 165 additions and 0 deletions

View file

@ -3,6 +3,7 @@
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
@ -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)

View file

@ -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) ────────────────────────────── */
/*

View file

@ -1,3 +1,4 @@
#include <signal.h>
#include <stdatomic.h>
#include <string.h>
#include <stdio.h>
@ -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);
}