chore: baseline snapshot — prior-session work + ha-consistency-invariants spec family
Pre-existing uncommitted work: ha-repl-hardening payload data plane (7.x), heartbeat liveness fix (Req 1.3), connection-reuse ASAN fixes, doc cleanup. New: ha-consistency-invariants umbrella (requirements/design/research) and its four implementation sub-specs (specs generated via batch, cross-spec review applied), steering roadmap + product.md Raft+CP decision.
This commit is contained in:
parent
084244941b
commit
06135954ff
88 changed files with 25611 additions and 5285 deletions
|
|
@ -540,7 +540,10 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
|
|||
ectx->votes[i].peer->hostname,
|
||||
(uint32_t)ectx->votes[i].peer->port);
|
||||
if (rc >= 0)
|
||||
TRACE("HA: repl registered standby ep[%d] (slot %d)", i, rc);
|
||||
INFO("HA: repl registered standby ep[%d] (slot %d, %s:%u)",
|
||||
i, rc,
|
||||
ectx->votes[i].peer->hostname,
|
||||
ectx->votes[i].peer->port);
|
||||
else
|
||||
WARN("HA: repl_add_standby ep[%d] failed: %d", i, rc);
|
||||
}
|
||||
|
|
@ -1489,6 +1492,38 @@ int ha_dispatch_repl_action_for_standby(ha_state_t *ha,
|
|||
* For CTRL_INIT / CTRL_ELECTING the allowlist is tighter: only
|
||||
* OP_requestVote and OP_probeConfigTag pass through.
|
||||
*/
|
||||
/* ha_has_write_quorum — check whether the cluster has enough connected
|
||||
* standbys to achieve majority for state-changing operations.
|
||||
*
|
||||
* Quorum = ceil(n_peers / 2) + 1 (self-vote included).
|
||||
* Example: n_peers=3 → quorum=2 → need ≥1 connected standby.
|
||||
*
|
||||
* Called from ha_dispatch_gate for CTRL_PRIMARY — if the cluster loses
|
||||
* majority (standby failures), write ops are rejected with -XCE_ERR_NOT_READY
|
||||
* to preserve the majority-consistency invariant.
|
||||
*
|
||||
* Read-only queries (joblist, reslist, hostlist, etc.) remain available
|
||||
* regardless of majority status — they don't mutate state. */
|
||||
static bool ha_has_write_quorum(ha_state_t *ha)
|
||||
{
|
||||
if (!ha)
|
||||
return false;
|
||||
|
||||
/* Single-node cluster: no standbys needed — self is the majority. */
|
||||
if (ha->n_peers <= 1)
|
||||
return true;
|
||||
|
||||
/* Count connected standbys. standby_endpoints is populated by
|
||||
* raft_cb_become_leader (promoted vote eps) and on_standby_connected
|
||||
* (late joiners); removed by error handlers. */
|
||||
int connected = ha->standby_endpoints
|
||||
? ha->standby_endpoints->count : 0;
|
||||
|
||||
/* Need at least n_peers/2 connected standbys to achieve quorum.
|
||||
* Integer division: n_peers/2 for n_peers=3 → 1 standby minimum. */
|
||||
return connected >= (int)(ha->n_peers / 2);
|
||||
}
|
||||
|
||||
int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
||||
{
|
||||
ctrl_role_t role =
|
||||
|
|
@ -1496,6 +1531,31 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
|
||||
switch (role) {
|
||||
case CTRL_PRIMARY:
|
||||
/* State-changing operations require majority so committed entries
|
||||
* are durable across failover. Read-only queries and replication
|
||||
* ops are always allowed. */
|
||||
if (!ha_has_write_quorum(ha)) {
|
||||
switch (opcode) {
|
||||
/* Replication ops — always allowed (they build majority) */
|
||||
case OPCode_heartbeat:
|
||||
case OPCode_replConfig:
|
||||
case OPCode_replConfigPush:
|
||||
case OPCode_upgradeConfigAck:
|
||||
case OPCode_hello:
|
||||
case OPCode_walAck:
|
||||
case OPCode_replEntry:
|
||||
case OPCode_replAck:
|
||||
case OPCode_replCatchUpDone:
|
||||
case OPCode_walCatchupEntry:
|
||||
case OPCode_snapshotTransfer:
|
||||
/* Read-only queries */
|
||||
case OPCode_get:
|
||||
case OPCode_probeConfigTag:
|
||||
return 0;
|
||||
default:
|
||||
return -XCE_ERR_NOT_READY;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case CTRL_INIT:
|
||||
|
|
@ -2623,12 +2683,18 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
|||
int peer_idx = vctx->peer_idx;
|
||||
uint64_t committed_lsn = 0;
|
||||
uint64_t committed_tag = 0;
|
||||
uint64_t last_written = 0; /* latest WAL LSN — may be > committed */
|
||||
#ifndef XCE_TESTING
|
||||
if (c) {
|
||||
committed_lsn = atomic_load_explicit(&c->persist.committed_lsn,
|
||||
memory_order_acquire);
|
||||
committed_tag = atomic_load_explicit(&ha->committed_config_tag,
|
||||
memory_order_acquire);
|
||||
/* current_lsn is the next LSN to write; last written = current - 1.
|
||||
* Read without lock — monotonic, approximate value is sufficient
|
||||
* for the replication decision. */
|
||||
last_written = (c->persist.current_lsn > 0)
|
||||
? c->persist.current_lsn - 1 : 0;
|
||||
}
|
||||
#else
|
||||
/* In test mode, c may be NULL; caller sets ha fields directly */
|
||||
|
|
@ -2658,8 +2724,15 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
|||
oldest_wal_lsn = ha_compute_oldest_wal_lsn(c->persist.wal_path);
|
||||
}
|
||||
#endif
|
||||
/* Replication target: use max(committed_lsn, last_written) so
|
||||
* standbys can catch up even when committed_lsn is stuck at 0
|
||||
* (no replicator-registered standbys to advance it). */
|
||||
uint64_t target_lsn = committed_lsn;
|
||||
if (last_written > target_lsn)
|
||||
target_lsn = last_written;
|
||||
|
||||
ha_dispatch_repl_action_for_standby(ha, c, ep, peer_idx,
|
||||
committed_tag, committed_lsn,
|
||||
committed_tag, target_lsn,
|
||||
oldest_wal_lsn);
|
||||
} else if (rep_s.data_which == RequestReply_data_walackreplymsg) {
|
||||
/* Task 4.1: WAL ACK reply from standby — extract lsn + success,
|
||||
|
|
@ -3750,8 +3823,8 @@ static void on_standby_connected(endpoint_t *ep)
|
|||
int rc = repl_add_standby(&c->persist.repl, ep,
|
||||
peer->hostname, (uint32_t)peer->port);
|
||||
if (rc >= 0)
|
||||
TRACE("HA: repl registered late-joiner standby peer[%d] (slot %d)",
|
||||
sctx->peer_idx, rc);
|
||||
INFO("HA: repl registered late-joiner standby peer[%d] (slot %d, %s:%u)",
|
||||
sctx->peer_idx, rc, peer->hostname, (uint32_t)peer->port);
|
||||
else
|
||||
WARN("HA: repl_add_standby late-joiner peer[%d] failed: %d",
|
||||
sctx->peer_idx, rc);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue