- 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)
206 lines
6.5 KiB
C
206 lines
6.5 KiB
C
#define _GNU_SOURCE
|
|
#include "ha.h"
|
|
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <signal.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;
|
|
}
|
|
|
|
/* ── 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)
|
|
{
|
|
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role, memory_order_acquire);
|
|
|
|
/* Only standbys watch for primary heartbeat timeout */
|
|
if (role != CTRL_STANDBY) return;
|
|
|
|
/* If we have no primary endpoint, nothing to monitor */
|
|
if (ha->primary_host[0] == '\0') return;
|
|
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
|
|
pthread_mutex_lock(&ha->lock);
|
|
struct timespec last = ha->last_heartbeat;
|
|
pthread_mutex_unlock(&ha->lock);
|
|
|
|
uint64_t elapsed_ms =
|
|
(uint64_t)(now.tv_sec - last.tv_sec) * 1000ULL +
|
|
(uint64_t)(now.tv_nsec - last.tv_nsec) / 1000000ULL;
|
|
|
|
if (elapsed_ms >= ha->config.heartbeat_timeout_ms) {
|
|
/* Primary appears dead — full promotion in P1-3a.
|
|
* For now: set failover_requested so watchdog thread can act. */
|
|
atomic_store_explicit(&ha->failover_requested, 1, memory_order_release);
|
|
}
|
|
}
|
|
|
|
static void *ha_watchdog_thread(void *arg)
|
|
{
|
|
ha_state_t *ha = arg;
|
|
|
|
while (!atomic_load_explicit(&ha->watchdog_stop, memory_order_acquire)) {
|
|
long ms = (long)ha->config.heartbeat_interval_ms;
|
|
struct timespec ts = {
|
|
.tv_sec = ms / 1000,
|
|
.tv_nsec = (ms % 1000) * 1000000L,
|
|
};
|
|
nanosleep(&ts, NULL);
|
|
|
|
/* Re-check stop flag after sleep */
|
|
if (atomic_load_explicit(&ha->watchdog_stop, memory_order_acquire)) break;
|
|
|
|
ha_check_heartbeat_nocluster(ha);
|
|
|
|
/* Handle manual failover request (SIGUSR1 path — P1-3a) */
|
|
if (atomic_load_explicit(&ha->failover_requested, memory_order_acquire)) {
|
|
atomic_store_explicit(&ha->failover_requested, 0, memory_order_release);
|
|
/* Full ha_promote_to_primary(c) call in P1-3a when cluster_t is available */
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int ha_start_watchdog(ha_state_t *ha)
|
|
{
|
|
atomic_store_explicit(&ha->watchdog_stop, 0, memory_order_release);
|
|
int rc = pthread_create(&ha->watchdog_thread, NULL, ha_watchdog_thread, ha);
|
|
if (rc != 0) return -rc;
|
|
return 0;
|
|
}
|
|
|
|
void ha_stop_watchdog(ha_state_t *ha)
|
|
{
|
|
atomic_store_explicit(&ha->watchdog_stop, 1, memory_order_release);
|
|
if (ha->watchdog_thread != 0) {
|
|
pthread_join(ha->watchdog_thread, NULL);
|
|
ha->watchdog_thread = 0;
|
|
}
|
|
}
|
|
|
|
void ha_fini(ha_state_t *ha)
|
|
{
|
|
if (!ha)
|
|
return;
|
|
ha_stop_watchdog(ha); /* join watchdog thread before destroying mutex */
|
|
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;
|
|
}
|