feat(ha-convergence-recovery): quorum transition logging — CAS edge-detect + 4 call sites (task 5.2)
This commit is contained in:
parent
3218d9ffaf
commit
07de4581b0
3 changed files with 84 additions and 12 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue