feat(ha-commit-barrier-rollout): config barrier with production ACK ingestion (task 5.2 + r1)

configAck branch: per-push majority (acked_config_lsn >= push_tag,
1+count > n_peers/2 over r->ce->n_peers, n_peers<=1 immediate,
ce==NULL -EINVAL); legacy >0 counting and n_active==0 shortcut dead.
push_config_to_standbys returns the barrier result; upgrade failure ->
send_upgrade_error (rc forwarded), no success ACK, no restart,
committed_config_tag success-only.

Remediation r1 (two CRITICAL review findings):
- F1: wire production config-ACK ingestion — replconfigpushreplymsg
  decode branch in repl_ack_decode_and_dispatch -> repl_on_config_ack
  on the promoted-vote ep; + defensive cluster.c OPCode_reply case.
  Closes the unlanded half of ha-config-bootstrap 8.1; without this the
  barrier would brick every multi-member config upgrade. No schema change.
- F2: keep the barrier off the iomgr thread — export
  push_config_transport_only (non-blocking) for the 5 ha.c + join.c
  streaming sites; barrier path now vtask-only (do_upgrade_config).

Wire-path test drives real capnp ACK bytes through the production decode
branch. 30/30 rollout, 6/6 repl_majority, 46/46 config_repl; census 0
FAIL; pre-existing fixture leak only (less than baseline).
This commit is contained in:
Rongsong Shen 2026-06-13 03:46:29 +00:00
parent 9cdeba5816
commit 5a2fae1d37
12 changed files with 1034 additions and 399 deletions

View file

@ -407,11 +407,16 @@ void repl_on_config_ack(replicator_t *r, repl_ep_t ep, uint64_t acked_config_lsn
* anything ever" counting with self-count and the zero-active
* success short-circuit is retired). Returns the engine code
* unmodified: 0 / -ETIMEDOUT / -XCE_ERR_NOT_READY / -EINVAL.
* configAck: legacy standby counting (hardened by barrier-rollout task 5):
* quorum = (n_active + 2) / 2 over active (non-catch-up)
* standbys; acked_total = standbys with acked_config_lsn > 0,
* NO self-count (config must be confirmed by standbys).
* Returns 0 on quorum, -ETIMEDOUT on timeout.
* configAck: per-push arithmetic mirroring the engine's rules on the
* config track (barrier-rollout task 5.2, design §Config
* Barrier): entry_lsn carries the in-flight config push tag;
* count = active (non-catch-up) standbys with
* acked_config_lsn >= push_tag (a previous upgrade's ack no
* longer counts); majority = 1 (self) + count > n_peers / 2
* over configured members (engine n_peers, set-once at
* wiring); n_peers <= 1 → immediate success. Returns 0 on
* quorum, -ETIMEDOUT on timeout, -EINVAL when the engine is
* not wired (fail-loud, mirroring the walAck branch).
* ============================================================ */
int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
@ -441,7 +446,33 @@ int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
return commit_submit(r->ce, entry_lsn, COMMIT_MODE_WAIT, timeout_ms);
}
/* ── configAck track (legacy standby counting; task 5 hardens it) ── */
/* ── configAck track (per-push arithmetic; barrier-rollout task 5.2) ──
* entry_lsn carries the in-flight config push tag (repl.h: the branch
* reuses the walAck adapter parameter — no second parameter). */
uint64_t push_tag = entry_lsn;
int n_peers;
if (r->ce == NULL) {
/* The engine is wired at cluster_init_phase2 before any config
* push reaches this wait; an unwired engine is a harness defect,
* not a quorum — fail loud rather than fabricate a result
* (mirrors the walAck branch's commit_submit -EINVAL). */
WARN("repl_wait_majority_ack: configAck wait without a wired"
" commit engine (push_tag=%016" PRIx64 ")", push_tag);
return -EINVAL;
}
/* ce and ce->n_peers are set-once before replication traffic
* (repl_set_commit_engine contract) — safe to read without locks. */
n_peers = r->ce->n_peers;
if (n_peers <= 1) {
/* Single-member cluster: the local save is the quorum. */
TRACE("repl_wait_majority_ack: configAck self-quorum"
" (n_peers=%d, push_tag=%016" PRIx64 ")", n_peers, push_tag);
return 0;
}
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
@ -458,8 +489,7 @@ int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
while (1) {
mylock_lock(&r->lock);
int n_active = 0;
int acked_count = 0;
int covered = 0;
for (int i = 0; i < MAX_STANDBYS; i++) {
standby_conn_t *s = &r->standbys[i];
@ -467,26 +497,18 @@ int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
continue;
if (atomic_load_explicit(&s->in_catch_up, memory_order_acquire))
continue;
n_active++;
if (atomic_load_explicit(&s->acked_config_lsn,
memory_order_acquire) > 0)
acked_count++;
memory_order_acquire) >= push_tag)
covered++;
}
mylock_unlock(&r->lock);
/* Single-node shortcircuit: no active standbys → local write is
* quorum (configAck only, Req 8.5 — walAck never reaches this
* loop; its single-member case is the engine's self-quorum). */
if (n_active == 0) {
rc = 0;
break;
}
int quorum = (n_active + 2) / 2;
int acked_total = acked_count; /* no self-count for configAck */
if (acked_total >= quorum) {
/* Majority over configured members, self counted: the primary's
* own save plus standbys covering the in-flight push tag. No
* n_active shortcut — a multi-member cluster with zero covering
* acks must time out, never fabricate success (Req 4.2). */
if (1 + covered > n_peers / 2) {
rc = 0;
break;
}