feat(ha-convergence-recovery): quorum transition logging — CAS edge-detect + 4 call sites (task 5.2)

This commit is contained in:
Rongsong Shen 2026-06-14 06:31:25 +00:00
parent 3218d9ffaf
commit 07de4581b0
3 changed files with 84 additions and 12 deletions

View file

@ -561,6 +561,11 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
}
}
/* Quorum-transition observability (Req 8.1, 8.2): promotion registered the
* surviving vote endpoints as standbys — the write-quorum count just changed.
* Record any loss/restore edge once (no-op when quorum is held at promotion). */
ha_log_quorum_transition(ha);
if (c) {
/* Push config to standbys — read raw bytes from disk so standbys
* can save the file. NULL bytes are passed only in test mode. */
@ -1509,6 +1514,11 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
* are durable across failover. Read-only queries and replication
* ops are always allowed. */
if (!ha_has_write_quorum(ha)) {
/* Quorum-transition observability backstop (Req 8.1, 8.3): record the
* loss edge if a membership-change site missed it. The edge detector
* makes this silent on every call after the first — repeated rejected
* operations do NOT produce a log storm. */
ha_log_quorum_transition(ha);
switch (opcode) {
/* Replication ops — always allowed (they build majority) */
case OPCode_heartbeat:
@ -3163,6 +3173,11 @@ static int vote_ep_err_handler(void *ep_raw)
* vctx->c may be NULL in test mode — guard accordingly. */
if (vctx->c)
repl_remove_standby(&vctx->c->persist.repl, ep);
/* Quorum-transition observability (Req 8.1): a standby just dropped — the
* write-quorum count changed. Record the loss edge once (silent if
* quorum is still held after the removal). */
ha_log_quorum_transition(ha);
} else {
/* Election phase: count as denied vote */
handle_request_vote_reply(ha, vctx->ctx, vctx->peer_idx,
@ -3819,6 +3834,11 @@ static void on_standby_connected(endpoint_t *ep)
sctx->peer_idx, rc);
}
/* Quorum-transition observability (Req 8.2): a late-joiner standby connected —
* the write-quorum count changed. Record the restore edge once (silent if
* quorum was already held). */
ha_log_quorum_transition(ha);
/* Set state-aware dispatcher for subsequent messages on this endpoint */
ep->msg_handler = peer_ctrl_msg_handler;
}

View file

@ -383,21 +383,72 @@ void cluster_apply_ctrlhello_reply(cluster_t *c, uint64_t committed_lsn,
}
/*
* ha_log_quorum_transition — RED scaffold (ha-convergence-recovery task 5.1).
* ha_log_quorum_transition — write-quorum loss/restore edge logger
* (ha-convergence-recovery task 5.2, Req 8.1, 8.2, 8.3).
*
* Task 5.2 replaces this body with the real edge detector: evaluate the current
* write-quorum state (ha_has_write_quorum, ha.c), compare against ha->quorum_lost,
* and on a transition CAS-flip the flag and emit exactly one WARN with the
* connected/required counts (Req 8.1, 8.2), invoking ha_quorum_transition_log_hook
* once under XCE_TESTING. Repeated calls with no state change emit nothing
* (Req 8.3); a later loss/restore cycle logs again (edge detector, not a latch).
* Evaluate the current write-quorum state and compare it against the
* ha_state_t.quorum_lost edge-detector flag. On a TRANSITION — and only on a
* transition — atomically flip the flag via CAS and emit exactly one WARN
* carrying the connected/required member counts:
* - loss: "HA quorum lost: connected=%d required=%d" (Req 8.1)
* - restore: "HA quorum restored: connected=%d required=%d" (Req 8.2)
*
* This RED stub does NOTHING: no edge detection, no flag flip, no WARN, no hook
* invocation — so the task 5.1 quorum-transition tests FAIL until 5.2 lands.
* The flag makes the logging an edge detector, NOT a latch: repeated calls while
* the state is unchanged emit nothing (Req 8.3 — no log storms across the
* dispatch-gate backstop's per-rejection calls), but a later loss/restore cycle
* logs again. The CAS guarantees once-per-transition under concurrency: only the
* caller that actually flips the flag emits the WARN.
*
* Observability only — this never returns a status and never gates an operation
* outcome; the dispatch gate's existing -XCE_ERR_NOT_READY behavior is unchanged.
*
* connected/required mirror the ha_has_write_quorum BODY (ha.c): with the repl
* back-pointer wired, connected = repl_active_standby_count(repl) and the
* threshold required = n_peers/2 (integer division — for n_peers=3 → 1). When
* the back-pointer is NULL (legacy contexts), connected falls back to the
* standby_endpoints count, matching the gate's own fallback exactly. The quorum
* decision math is NOT changed here — this only reports what that body decides.
*/
void ha_log_quorum_transition(ha_state_t *ha)
{
(void)ha; /* RED scaffold — task 5.2 implements the edge detection. */
if (ha == NULL)
return;
/* Single-node clusters always hold quorum (ha_has_write_quorum returns true
* for n_peers <= 1); there is no transition to observe. */
if (ha->n_peers <= 1)
return;
/* Mirror ha_has_write_quorum's connected-count derivation (ha.c body). */
int connected;
if (ha->repl != NULL)
connected = repl_active_standby_count(ha->repl);
else
connected = ha->standby_endpoints ? ha->standby_endpoints->count : 0;
int required = (int)(ha->n_peers / 2);
int lost_now = (connected < required) ? 1 : 0;
/* Edge detection: only the caller that flips quorum_lost from its prior value
* to lost_now emits. CAS expected=!lost_now → desired=lost_now; if the flag
* already equals lost_now (no transition) the CAS fails and we return
* silently (Req 8.3). */
int expected = lost_now ? 0 : 1;
if (!atomic_compare_exchange_strong_explicit(
&ha->quorum_lost, &expected, lost_now,
memory_order_acq_rel, memory_order_acquire))
return; /* no edge (or another thread already logged it) — no WARN */
if (lost_now)
WARN("HA quorum lost: connected=%d required=%d", connected, required);
else
WARN("HA quorum restored: connected=%d required=%d", connected, required);
#ifdef XCE_TESTING
/* Emission-count seam (Req 8.1-8.3 observable): invoked once per WARN. */
if (ha_quorum_transition_log_hook != NULL)
ha_quorum_transition_log_hook(lost_now, connected, required);
#endif
}
#ifdef XCE_TESTING