feat(ha-commit-engine): engine instantiation + sync-append path switch (task 5.3)

cluster_t embeds commit_engine_t; init in cluster_init_phase2 after the
vtaskmgr guard (idempotence-guarded — phase2 can run twice), wiring
repl_set_commit_engine + ha.repl back-pointer before any standby can
register; fini after iomgr/vtask shutdown. persist_wal_sync_append
passes written_lsn; engine-wired path drops -ENODEV and the
committed_lsn store (engine owns advancement); LSN-0 first-entry
routes to legacy per the accepted interim. p2 -ENODEV pin updated to
the Req 6.3 contract. 34/34 + 6/6 green through honest delegation;
census exactly at the 3 validated baselines; production binary links.
This commit is contained in:
Rongsong Shen 2026-06-12 00:16:22 +00:00
parent 6600fd5d1f
commit f0d6074d4e
6 changed files with 142 additions and 50 deletions

View file

@ -4,6 +4,7 @@
#include "ctrl_metrics.h"
#include "hook.h"
#include "xce/cluster_def.h"
#include "xce/errnox.h"
#include "xce/types.h"
#include "controller.capnp.h"
#include "xlist.h"
@ -1011,44 +1012,71 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
return rc;
}
/* Majority-commit replication (Req 5.1–5.3).
* After local fsync via persist_wal_async, wait for majority quorum ACK.
* On quorum: advance committed_lsn. On no standbys or timeout: degraded.
/* Majority-commit replication (Req 5.1–5.3; ha-commit-engine Req 1.1–1.3).
* After local fsync via persist_wal_async, confirm per-entry majority.
* We use current_lsn - 1 because persist_wal_async already incremented it. */
uint64_t written_lsn;
pthread_mutex_lock(&p->lock);
written_lsn = p->current_lsn - 1;
pthread_mutex_unlock(&p->lock);
/* No standbys registered → degraded mode immediately (no quorum to wait for) */
mylock_lock(&p->repl.lock);
int has_standbys = (p->repl.nstandbys > 0);
mylock_unlock(&p->repl.lock);
int sync_err;
if (!has_standbys) {
sync_err = -ENODEV;
} else {
/* entry_lsn = 0: mechanical signature update only (ha-commit-engine
* task 5.1) — the legacy body ignores it. Task 5.3 passes
* written_lsn so the wired engine evaluates per-entry majority. */
sync_err = repl_wait_majority_ack(&p->repl, 0, p->config.sync_timeout_ms,
/* Engine-wired path (ha-commit-engine task 5.3): delegate the commit
* decision to the commit engine through the walAck adapter. The engine
* is the single committed_lsn writer (no store here), and its
* n_peers<=1 self-quorum replaces the old has_standbys → -ENODEV
* degraded special case (Req 6.3).
*
* LSN-0 edge (ACCEPTED interim behavior — spec Implementation Notes):
* written_lsn is passed unconditionally, but the FIRST-EVER WAL entry
* of a fresh cluster is LSN 0 and commit_submit rejects lsn==0, so the
* adapter's entry_lsn != 0 guard routes that one entry to the legacy
* branch below. This happens once per cluster lifetime;
* ha-single-leader-role's promotion no-op entry eliminates it in
* practice. */
if (p->repl.ce != NULL && written_lsn != 0) {
sync_err = repl_wait_majority_ack(&p->repl, written_lsn,
p->config.sync_timeout_ms,
(uint8_t)OPCode_walAck);
if (sync_err == 0)
atomic_store_explicit(&p->committed_lsn, written_lsn, memory_order_release);
else
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. */
mylock_lock(&p->repl.lock);
int has_standbys = (p->repl.nstandbys > 0);
mylock_unlock(&p->repl.lock);
if (!has_standbys) {
/* No standbys registered → degraded mode (no quorum to wait for) */
sync_err = -ENODEV;
} else {
sync_err = repl_wait_majority_ack(&p->repl, written_lsn,
p->config.sync_timeout_ms,
(uint8_t)OPCode_walAck);
if (sync_err == 0)
atomic_store_explicit(&p->committed_lsn, written_lsn,
memory_order_release);
else
CTRL_METRIC_INC(ctrl_repl_degraded_count);
}
}
if (sync_rc_out)
*sync_rc_out = sync_err;
/* sync_err < 0 — replication failed.
* -ENODEV: no standbys registered (single-node or degraded).
* -ETIMEDOUT: standbys were registered but didn't ACK in time.
* Only -ENODEV is acceptable for forward progress; timeout means
* the cluster had standbys but couldn't reach quorum — the write
* must be rejected to preserve the majority-consistency invariant. */
if (sync_err == -ETIMEDOUT)
return -ETIMEDOUT;
* -ENODEV: legacy degraded mode (no standbys, engine-less) — acceptable
* for forward progress.
* -ETIMEDOUT: no per-entry majority within the deadline — the write
* must be rejected to preserve the majority-consistency
* invariant.
* -XCE_ERR_NOT_READY: engine admission cap exhausted — retriable
* rejection.
* Timeout and admission rejection propagate to the job-submit error
* path (ha-commit-engine Req 1.1, 4.4). */
if (sync_err == -ETIMEDOUT || sync_err == -XCE_ERR_NOT_READY)
return sync_err;
return 0;
}