feat(ha-commit-barrier-rollout): persist_commit_barrier + legacy walAck retirement (task 2.2)

Barrier: watermark = current_lsn - 1 under p->lock (released before
blocking), covered/nothing-appended/LSN-0 short-circuits, sync_timeout_ms
fallback, engine code unmodified, ce==NULL vacuous. repl.c walAck track
now engine-only (legacy acked_lsn>0 counting and walAck-reachable
n_active==0 success deleted); configAck track byte-equivalent for
task 5. 15/15 group; census 472/472.
This commit is contained in:
Rongsong Shen 2026-06-12 05:36:42 +00:00
parent 611fbc8d7b
commit 8f688eace1
5 changed files with 106 additions and 54 deletions

View file

@ -1042,10 +1042,13 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
if (sync_err != 0)
CTRL_METRIC_INC(ctrl_repl_degraded_count);
} else {
/* DEPRECATED legacy branch: engine-less (ce == NULL) unit-test
* contexts plus the LSN-0 first entry. Connectivity-based counting;
* committed_lsn is stored here only because no engine owns it.
* Retired by ha-commit-barrier-rollout. */
/* Residual non-engine branch: engine-less (ce == NULL) unit-test
* contexts plus the LSN-0 first entry. The legacy connectivity-
* based counting was retired by ha-commit-barrier-rollout task 2.2:
* repl_wait_majority_ack now confirms LSN 0 immediately (documented
* once-per-lifetime edge) and surfaces engine-less walAck waits as
* -EINVAL via commit_submit, so the committed_lsn store below only
* fires for the LSN-0 case (a no-op store of 0). */
mylock_lock(&p->repl.lock);
int has_standbys = (p->repl.nstandbys > 0);
mylock_unlock(&p->repl.lock);
@ -1171,19 +1174,58 @@ wal_commit_mode_t wal_classify(uint32_t wal_type, int detail)
}
}
/* Commit barrier helper — INERT RED stub (ha-commit-barrier-rollout task
* 2.1; implemented in task 2.2). Returns a wrong sentinel so the task-2.1
* tests fail on assertions, not linking. Task-2.2 contract (design.md
* §Commit Barrier Helper): snapshot the appended watermark (current_lsn - 1
* under p->lock), return 0 when committed_lsn already covers it (or nothing
* appended), else commit_submit(WAIT, timeout_ms ? timeout_ms :
* p->config.sync_timeout_ms) with the engine code passed through
* unmodified. */
/* Commit barrier helper (ha-commit-barrier-rollout task 2.2, design.md
* §Commit Barrier Helper; Req 1.1, 1.2, 1.3).
*
* Handler-callable WAIT completion on the appended watermark: snapshot the
* last-appended LSN, return 0 when the commit index already covers it,
* else block in the commit engine until coverage or deadline. The engine
* result is returned UNMODIFIED (0 / -ETIMEDOUT / -XCE_ERR_NOT_READY /
* -EINVAL) so callers map it to their own error replies.
*
* Lock discipline: p->lock is held only for the current_lsn snapshot and
* is released BEFORE commit_submit(WAIT) — the engine blocks on ce->lock
* + condvar, and p->lock must never be held across it (engine
* lock-ordering rule). */
int persist_commit_barrier(persist_t* p, uint32_t timeout_ms)
{
(void)p;
(void)timeout_ms;
return -ENOSYS;
uint64_t watermark;
if (p == NULL)
return -EINVAL;
/* Snapshot the last-appended watermark under p->lock. current_lsn is
* the NEXT LSN to assign (persist.h invariant: committed_lsn <=
* current_lsn - 1), so the highest appended entry is current_lsn - 1
* (resolved contract, spec Implementation Notes 2.1). current_lsn ==
* 0 means nothing was ever appended — vacuously satisfied. */
pthread_mutex_lock(&p->lock);
if (p->current_lsn == 0) {
pthread_mutex_unlock(&p->lock);
return 0;
}
watermark = p->current_lsn - 1;
pthread_mutex_unlock(&p->lock);
/* Already covered — committed_lsn is monotone, so coverage observed
* here can never regress. This check also absorbs the LSN-0 first
* entry (watermark 0 vs committed_lsn 0): that once-per-lifetime entry
* has no quorum contract (ha-commit-engine Implementation Notes) and
* commit_submit would reject lsn == 0 anyway. */
if (atomic_load_explicit(&p->committed_lsn, memory_order_acquire) >=
watermark)
return 0;
/* Engine not wired: legacy/bootstrap contexts have no engine commit
* contract — there is nothing to wait on, so the barrier is a no-op.
* Production always wires the engine (cluster_init_phase2). */
if (p->repl.ce == NULL)
return 0;
/* timeout_ms == 0 borrows the configured sync deadline (Req 1.2). */
return commit_submit(p->repl.ce, watermark, COMMIT_MODE_WAIT,
timeout_ms != 0 ? timeout_ms
: p->config.sync_timeout_ms);
}
/* Classified append at the hook seam (barrier-rollout task 1.2).

View file

@ -399,17 +399,19 @@ void repl_on_config_ack(replicator_t *r, repl_ep_t ep, uint64_t acked_config_lsn
}
/* ============================================================
* Majority-quorum ACK wait (task 7.3)
* Majority-quorum ACK wait (task 7.3; walAck track rebased onto the
* commit engine by ha-commit-barrier-rollout task 2.2)
*
* Quorum formula: quorum = (n_active + 2) / 2 [ceiling of (n_active+1)/2]
* n_active = count of active (non-catch-up) standbys.
*
* walAck: acked_total = 1 (self) + standbys with acked_lsn > 0.
* Primary counts itself — it persists WAL before broadcasting.
* configAck: acked_total = standbys with acked_config_lsn > 0 (no self-count).
* Config must be confirmed by standbys; primary does not self-count.
*
* Returns 0 on quorum, -ETIMEDOUT on timeout.
* walAck: per-LSN commit evaluation, delegated to the commit engine
* unconditionally (the legacy connectivity-based "ACKed
* 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.
* ============================================================ */
int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
@ -418,27 +420,28 @@ int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
if (r == NULL)
return -EINVAL;
int is_wal_ack = (ack_op == (uint8_t)OPCode_walAck);
if (ack_op == (uint8_t)OPCode_walAck) {
/* LSN-0 edge: the first-ever WAL entry of a fresh cluster is LSN 0
* (current_lsn starts at 0; written_lsn = current_lsn - 1) and is
* structurally un-commitable — commit_submit rejects lsn == 0.
* Documented once-per-cluster-lifetime interim behavior
* (ha-commit-engine Implementation Notes): confirm it immediately;
* the promotion no-op entry (ha-single-leader-role) eliminates the
* case in practice. configAck callers also pass entry_lsn = 0 but
* are dispatched by ack_op above, never through this guard. */
if (entry_lsn == 0)
return 0;
/* Engine adapter (ha-commit-engine task 5.2, design §Job-Submit Adapter):
* the walAck barrier is per-LSN commit evaluation owned by the commit
* engine when wired. Caller holds no repl locks here, so the engine's
* ce->lock -> r->lock order is preserved.
*
* entry_lsn == 0 falls through to legacy counting: persist.c's
* persist_wal_sync_append still passes 0 until task 5.3 forwards
* written_lsn (and commit_submit rejects lsn==0 with -EINVAL). Note the
* first-ever WAL entry IS LSN 0 — structurally un-commitable under the
* current numbering; task 5.3 documents the resolution, and the
* promotion no-op entry (ha-single-leader-role) eliminates it in
* practice. configAck callers pass entry_lsn=0 and never delegate. */
if (is_wal_ack && r->ce != NULL && entry_lsn != 0) {
/* Unconditional engine delegation (ha-commit-engine task 5.2,
* design §Job-Submit Adapter). Caller holds no repl locks here, so
* the engine's ce->lock -> r->lock order is preserved. ce == NULL
* (engine never wired) is unreachable in production — the engine
* is wired at cluster_init_phase2 — and commit_submit surfaces it
* as -EINVAL rather than this adapter fabricating a quorum result. */
return commit_submit(r->ce, entry_lsn, COMMIT_MODE_WAIT, timeout_ms);
}
/* DEPRECATED legacy counting below (connectivity-based, not per-LSN):
* survives only for engine-less (ce == NULL) unit-test contexts and the
* configAck branch; retired by ha-commit-barrier-rollout. */
/* ── configAck track (legacy standby counting; task 5 hardens it) ── */
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
@ -465,27 +468,23 @@ int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
if (atomic_load_explicit(&s->in_catch_up, memory_order_acquire))
continue;
n_active++;
if (is_wal_ack) {
if (atomic_load_explicit(&s->acked_lsn, memory_order_acquire) > 0)
acked_count++;
} else {
if (atomic_load_explicit(&s->acked_config_lsn,
memory_order_acquire) > 0)
acked_count++;
}
if (atomic_load_explicit(&s->acked_config_lsn,
memory_order_acquire) > 0)
acked_count++;
}
mylock_unlock(&r->lock);
/* Single-node shortcircuit: no active standbys → local write is quorum.
* Applies to both walAck (self=1, quorum=1) and configAck (Req 8.5). */
/* 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 = is_wal_ack ? (1 + acked_count) : acked_count;
int acked_total = acked_count; /* no self-count for configAck */
if (acked_total >= quorum) {
rc = 0;