feat(ha-config-bootstrap): implement EPOLLRDHUP failover and heartbeat timeout vtask (task 9.4)

This commit is contained in:
Rongsong Shen 2026-05-29 07:17:55 +00:00
parent 9bbff7b016
commit 0d5f2cb1bb
6 changed files with 434 additions and 1 deletions

View file

@ -1524,3 +1524,165 @@ int ha_find_and_connect_to_primary(ha_state_t *ha,
? HA_FIND_PRIMARY_T3_MAX_MS : backoff_ms * 2;
}
}
/* ── Task 9.4: EPOLLRDHUP Failover and Heartbeat Timeout ───────────────────
*
* Requirements: 6.2 (RDHUP fast path), 6.3 (heartbeat timeout fallback),
* 6.4 (no heartbeat within timeout → elect), 6.8 (role flip)
* 10.5 (INFO logs on FSM transitions)
*/
void ha_on_primary_ctrl_fd_rdhup(ha_state_t *ha, cluster_t *c)
{
if (!ha) return;
/* Close and null the primary control fd */
if (ha->primary_ctrl_fd >= 0) {
close(ha->primary_ctrl_fd);
ha->primary_ctrl_fd = -1;
}
/* Transition to CTRL_ELECTING (Req 6.2) */
atomic_store_explicit(&ha->role, CTRL_ELECTING, memory_order_release);
INFO("HA: primary_ctrl_fd RDHUP → CTRL_ELECTING (Req 10.5)");
if (!c) return; /* unit test mode: skip persist + election */
/* Truncate uncommitted WAL entries above committed_lsn (Req 6.2) */
uint64_t committed = atomic_load_explicit(&ha->last_known_committed_lsn,
memory_order_acquire);
if (committed > 0)
persist_truncate_wal_above(&c->persist, committed);
/* Fire election — am_i_primary_quorum handles the vote and role assignment */
if (c->myconf && c->myconf->peers_cfg.candidates_count > 0) {
const xce_cluster_config_t *cfg = &c->myconf->peers_cfg;
int n = cfg->candidates_count > 0 ? cfg->candidates_count - 1 : 0;
/* Build peer pointer array (exclude self) */
xce_candidate_t *peer_ptrs[MAX_HA_CANDIDATES];
int np = 0;
for (int i = 0; i < cfg->candidates_count && np < n; i++) {
if (strcmp(cfg->candidates[i].name, c->myhost ? c->myhost->name : "") != 0)
peer_ptrs[np++] = &cfg->candidates[i];
}
uint64_t my_tag = c->myconf->config_version_tag;
uint64_t my_lsn = atomic_load_explicit(&c->persist.committed_lsn,
memory_order_acquire);
ctrl_role_t new_role = am_i_primary_quorum(ha, np, peer_ptrs, my_tag, my_lsn);
if (new_role == CTRL_PRIMARY)
INFO("HA: RDHUP recovery → CTRL_PRIMARY (Req 6.8)");
else
INFO("HA: RDHUP recovery → CTRL_STANDBY, reconnecting (Req 6.8)");
}
}
/* ── Heartbeat timeout watchdog ─────────────────────────────────────────── */
typedef struct {
ha_state_t *ha;
cluster_t *c;
sem_t reset_sem; /* ha_on_heartbeat_received posts here */
atomic_int disarmed; /* set to 1 by ha_disarm_heartbeat_timeout_vtask */
pthread_t thread;
int started;
} hb_vtask_t;
/* File-scope pointer to the active heartbeat watchdog.
* Safety: arm/disarm/heartbeat are called only from the single-threaded
* vtaskmgr dispatch context; no locking is needed in the current architecture.
* If concurrent access is added later, protect with ha->lock. */
static hb_vtask_t *s_hb_vtask = NULL;
static void *hb_timeout_thread(void *arg)
{
hb_vtask_t *t = (hb_vtask_t *)arg;
ha_state_t *ha = t->ha;
while (!atomic_load(&t->disarmed)) {
/* Compute deadline: election_timeout_ms + rand_jitter */
uint32_t jitter_ms = 0;
if (ha->config.election_jitter_max_ms > 0) {
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
unsigned int seed = (unsigned int)now.tv_nsec;
jitter_ms = (uint32_t)(rand_r(&seed) % ha->config.election_jitter_max_ms);
}
uint32_t total_ms = ha->config.election_timeout_ms + jitter_ms;
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += (long)(total_ms / 1000);
deadline.tv_nsec += (long)(total_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_sec++;
deadline.tv_nsec -= 1000000000L;
}
/* Wait for heartbeat signal or deadline */
int rc = sem_timedwait(&t->reset_sem, &deadline);
if (rc == -1 && errno == ETIMEDOUT) {
/* Timeout with no heartbeat → failover (Req 6.3, 6.4) */
if (!atomic_load(&t->disarmed)) {
WARN("HA: heartbeat timeout (%ums) → EPOLLRDHUP failover path",
total_ms);
ha_on_primary_ctrl_fd_rdhup(ha, t->c);
break; /* fired; thread exits */
}
}
/* heartbeat received — loop and re-arm deadline */
}
return NULL;
}
void ha_arm_heartbeat_timeout_vtask(ha_state_t *ha, cluster_t *c)
{
if (!ha) return;
/* Disarm any existing watchdog */
if (s_hb_vtask && s_hb_vtask->started) {
atomic_store(&s_hb_vtask->disarmed, 1);
sem_post(&s_hb_vtask->reset_sem); /* wake sleeping thread */
pthread_join(s_hb_vtask->thread, NULL);
sem_destroy(&s_hb_vtask->reset_sem);
free(s_hb_vtask);
s_hb_vtask = NULL;
}
hb_vtask_t *t = calloc(1, sizeof(*t));
if (!t) return;
t->ha = ha;
t->c = c;
t->started = 0;
atomic_init(&t->disarmed, 0);
sem_init(&t->reset_sem, 0, 0);
if (pthread_create(&t->thread, NULL, hb_timeout_thread, t) != 0) {
sem_destroy(&t->reset_sem);
free(t);
return;
}
t->started = 1;
s_hb_vtask = t;
}
void ha_on_heartbeat_received(ha_state_t *ha)
{
(void)ha;
if (s_hb_vtask && s_hb_vtask->started)
sem_post(&s_hb_vtask->reset_sem);
}
void ha_disarm_heartbeat_timeout_vtask(ha_state_t *ha)
{
(void)ha;
if (!s_hb_vtask) return;
if (s_hb_vtask->started) {
atomic_store(&s_hb_vtask->disarmed, 1);
sem_post(&s_hb_vtask->reset_sem);
pthread_join(s_hb_vtask->thread, NULL);
sem_destroy(&s_hb_vtask->reset_sem);
}
free(s_hb_vtask);
s_hb_vtask = NULL;
}