fix(ha): bug-1435 raft_lock serializes concurrent raft_state_t access
raft-drive-async's default-ON pool dispatch lets messages from different peer endpoints (two vote replies, or a vote reply racing a vote request) run concurrently on different vtask pool workers. raft_state_t's fields (votes_granted, replies_received, election_decided, term, role) are plain non-atomic — the raft library assumes a single-threaded caller. Two concurrent raft_on_vote_reply() calls could race: one reaches quorum and promotes to PRIMARY while the other, reading pre-increment vote count, independently satisfies "all-replied, no quorum" and fires on_become_follower for the same term, silently reverting the promotion — every request afterward correctly (but wrongly) rejected as STANDBY. Add ha->raft_lock (PTHREAD_MUTEX_RECURSIVE — a WON/quorum outcome can legitimately re-enter the state machine synchronously on the same thread, e.g. PreVote WON -> real election) around every raft_state_t entry point on the election/vote path. Verified via 40-round sequential and 40-round chaotic HA soak, zero committed-job loss, all 3 nodes led.
This commit is contained in:
parent
976a6c956b
commit
510b566bbf
3 changed files with 121 additions and 5 deletions
|
|
@ -545,7 +545,11 @@ static int raft_election_timeout_fn(void *arg)
|
|||
free(t);
|
||||
return 0;
|
||||
}
|
||||
/* bug-1435: serialize against concurrent vote-reply/vote-request
|
||||
* processing on the pool (see ha.h raft_lock doc). */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_on_election_timeout(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
free(t);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -612,7 +616,11 @@ static void raft_cb_cancel_timeout(void *ctx)
|
|||
static int raft_election_start_vtask_entry(vtask_t *task)
|
||||
{
|
||||
ha_state_t *ha = (ha_state_t *)task->data->u.generic;
|
||||
/* bug-1435: serialize against concurrent vote-reply/vote-request
|
||||
* processing on the pool (see ha.h raft_lock doc). */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_check_and_start_election(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -634,7 +642,9 @@ static void ha_dispatch_election_start(ha_state_t *ha)
|
|||
(void *)pool, (void *)data, (void *)task);
|
||||
free(data);
|
||||
free(task);
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_check_and_start_election(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -647,7 +657,9 @@ static void ha_dispatch_election_start(ha_state_t *ha)
|
|||
"election inline (stay alive)");
|
||||
free(data);
|
||||
free(task);
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_check_and_start_election(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2177,6 +2189,22 @@ int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority)
|
|||
/* failover_requested / watchdog_stop deleted — watchdog replaced by ha_arm_heartbeat_timeout_vtask */
|
||||
|
||||
pthread_mutex_init(&ha->lock, NULL);
|
||||
/* bug-1435: dedicated lock serializing raft_state_t's election/vote entry
|
||||
* points against concurrent pool-worker dispatch (see ha.h field doc).
|
||||
* RECURSIVE: a WON/quorum outcome legitimately re-enters the state machine
|
||||
* synchronously on the SAME thread (e.g. raft_on_pre_vote_reply's WON
|
||||
* branch -> ha_enter_real_election -> ha_start_election_async ->
|
||||
* raft_start_election, all before the outer call returns) — that is a
|
||||
* same-thread call chain, not a race, and must not self-deadlock. A plain
|
||||
* mutex would deadlock on that legitimate nesting; PTHREAD_MUTEX_RECURSIVE
|
||||
* still fully serializes the DIFFERENT-thread case this lock exists to fix. */
|
||||
{
|
||||
pthread_mutexattr_t raft_lock_attr;
|
||||
pthread_mutexattr_init(&raft_lock_attr);
|
||||
pthread_mutexattr_settype(&raft_lock_attr, PTHREAD_MUTEX_RECURSIVE);
|
||||
pthread_mutex_init(&ha->raft_lock, &raft_lock_attr);
|
||||
pthread_mutexattr_destroy(&raft_lock_attr);
|
||||
}
|
||||
|
||||
ha->ssl_ctx = NULL;
|
||||
ha->jitter_seed = 0;
|
||||
|
|
@ -2360,6 +2388,7 @@ void ha_fini(ha_state_t *ha)
|
|||
ha_cancel_promotion_fence_timer(ha);
|
||||
|
||||
pthread_mutex_destroy(&ha->lock);
|
||||
pthread_mutex_destroy(&ha->raft_lock);
|
||||
sem_destroy(&ha->heartbeat_seen);
|
||||
|
||||
/* Fix ASAN leak: free the SSL context if one was allocated */
|
||||
|
|
@ -5501,9 +5530,27 @@ static void vote_reply_dispatch(ha_state_t *ha, ha_elect_ctx_t *ctx, int idx,
|
|||
* never reach quorum on that peer's vote alone. ha_repl_ack_abs_peer_idx
|
||||
* (ha.c:3779) already implements the exact inverse of
|
||||
* raft_cb_send_vote_request's votes_idx remap and is unit-tested; reuse
|
||||
* it here (ep_is_vote=1) rather than duplicating the arithmetic. */
|
||||
* it here (ep_is_vote=1) rather than duplicating the arithmetic.
|
||||
*
|
||||
* bug-1435: raft_on_vote_reply mutates raft_state_t's plain (non-atomic)
|
||||
* votes_granted/replies_received/election_decided fields and can fire
|
||||
* on_become_leader/on_become_follower synchronously. With raft-drive-async
|
||||
* pool dispatch (default ON), a DIFFERENT peer's vote reply can be
|
||||
* processed on a DIFFERENT pool worker concurrently — without raft_lock,
|
||||
* two concurrent calls can race: one reaches quorum and promotes to
|
||||
* PRIMARY, while the other, racing on the pre-increment vote count,
|
||||
* independently satisfies the "all replied, no quorum" branch and
|
||||
* immediately fires on_become_follower for the same term, silently
|
||||
* reverting role to STANDBY right after promotion (verified data race —
|
||||
* every request thereafter is correctly, faithfully rejected as STANDBY
|
||||
* by ha_dispatch_gate, matching the observed symptom exactly). Hold
|
||||
* raft_lock across the whole call so the state-machine evaluation and any
|
||||
* callback it fires are atomic with respect to other raft_state_t
|
||||
* entry points. */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_on_vote_reply(&ha->raft, ha_repl_ack_abs_peer_idx(ha, /*ep_is_vote=*/1, idx),
|
||||
vote_granted, term);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -5755,8 +5802,13 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx,
|
|||
* instead of retrying AppendEntries against a follower that will
|
||||
* reject every entry forever. raft_on_higher_term_observed only
|
||||
* touches (term, voted_for, role) — never peer-indexed
|
||||
* bookkeeping — so it is safe to call before peer_idx is resolved. */
|
||||
* bookkeeping — so it is safe to call before peer_idx is resolved.
|
||||
* bug-1435: serialize against concurrent vote-reply/vote-request
|
||||
* processing (see ha.h raft_lock doc) — this call can also fire
|
||||
* on_become_follower, racing the same fields. */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
(void)raft_on_higher_term_observed(&ha->raft, sr.term);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
|
||||
/* peer_idx from standby_ep_ctx_t (layout-compatible with vote_ep_ctx_t) */
|
||||
int peer_idx = vctx->peer_idx;
|
||||
|
|
@ -5852,8 +5904,12 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx,
|
|||
/* bug-1421: a WAL NACK can also carry a higher term (the
|
||||
* standby rejected this entry as stale) — route it through
|
||||
* the same term-discovery path as the heartbeat state report
|
||||
* so the primary steps down instead of retrying forever. */
|
||||
* so the primary steps down instead of retrying forever.
|
||||
* bug-1435: serialize against concurrent raft_state_t access
|
||||
* (see ha.h raft_lock doc). */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
(void)raft_on_higher_term_observed(&ha->raft, reply_term);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
WARN("repl_ack_decode_and_dispatch: WAL NACK from standby"
|
||||
" lsn=%lu success=0 — ignoring", (unsigned long)acked_lsn);
|
||||
}
|
||||
|
|
@ -6993,8 +7049,13 @@ void ha_start_prevote_async(ha_state_t *ha, cluster_t *c)
|
|||
ha->raft.term + 1, n_peers);
|
||||
|
||||
/* raft_begin_pre_vote: self-grant + per-peer send; mutates only the
|
||||
* pre_vote_* tally in raft_state_t (no term/voted_for/role change). */
|
||||
* pre_vote_* tally in raft_state_t (no term/voted_for/role change).
|
||||
* bug-1435: serialize against concurrent raft_state_t access (see ha.h
|
||||
* raft_lock doc) — pre_votes_granted/pre_vote_replies[] are just as
|
||||
* unprotected as the real-vote tally. */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
int rc = raft_begin_pre_vote(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
|
||||
/* Arm the round timeout (bounded by election_timeout_ms). Production only;
|
||||
* the round state is in raft_state_t so tests exercise the branches via the
|
||||
|
|
@ -7051,7 +7112,14 @@ void ha_start_prevote_async(ha_state_t *ha, cluster_t *c)
|
|||
static void pre_vote_reply_dispatch(ha_state_t *ha, cluster_t *c,
|
||||
int peer_idx, int granted)
|
||||
{
|
||||
/* bug-1435: serialize against concurrent raft_state_t access (see ha.h
|
||||
* raft_lock doc). raft_lock is RECURSIVE: the WON branch below calls
|
||||
* ha_enter_real_election -> ... -> raft_start_election synchronously on
|
||||
* this same thread, which re-acquires raft_lock at its own call site —
|
||||
* safe by design (same-thread nesting), never blocks a legitimate chain. */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
int outcome = raft_on_pre_vote_reply(&ha->raft, peer_idx, granted);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
|
||||
if (outcome == RAFT_PRE_VOTE_WON) {
|
||||
ha_prevote_cancel_round_timeout();
|
||||
|
|
@ -7289,7 +7357,14 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
|
|||
ha_elect_ctx_release(ctx);
|
||||
return;
|
||||
}
|
||||
/* bug-1435: serialize against concurrent raft_state_t access (see ha.h
|
||||
* raft_lock doc). This call can fire on_become_leader synchronously
|
||||
* (single-node case) or on_send_vote_request per peer + on_schedule_
|
||||
* timeout (multi-node) — none of which re-enter a raft_* call, so the
|
||||
* lock is held only across this one call. */
|
||||
pthread_mutex_lock(&ha->raft_lock);
|
||||
raft_start_election(&ha->raft);
|
||||
pthread_mutex_unlock(&ha->raft_lock);
|
||||
|
||||
/* For single-node, raft_start_election fires on_become_leader synchronously.
|
||||
* on_become_leader (raft_cb_become_leader) handles role -> CTRL_PRIMARY and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue