feat(ha-commit-durability): cap commit self-contribution at durable_lsn + durable-advance commit wake (task 2.2)

This commit is contained in:
Rongsong Shen 2026-06-18 06:29:47 +00:00
parent cc83caf5a0
commit c41edc515e
10 changed files with 395 additions and 13 deletions

View file

@ -80,11 +80,23 @@ static uint64_t majority_candidate_locked(commit_engine_t* ce)
{
replicator_t* r;
uint64_t cand;
uint64_t cap = highest_appended_lsn(ce->p);
/* DURABILITY PRECONDITION (ha-commit-durability Req 1.2): the PRIMARY's
* self-contribution to the commit majority is capped at its DURABLE
* watermark (the highest LSN fdatasync'd locally), NOT the appended tip.
* An entry that is only in the page cache (appended but not yet synced)
* must not count self toward its majority — otherwise "committed" could be
* durable on a single member and lost across failover (bug-661). The
* standby side already persists-before-ACK (persist_wal_write_replicated),
* so once self is durable-capped, committed_lsn ⇒ durable on a majority.
* highest_appended_lsn is still used below as the never-beyond-the-log
* bound (it can only exceed durable_lsn, so it never loosens this cap). */
uint64_t cap = persist_durable_lsn(ce->p);
int n = 0;
int need, k, i;
/* Standalone / single-member: self constitutes the majority (Req 6.3). */
/* Standalone / single-member: self constitutes the majority (Req 6.3) —
* but self still only counts up to what is durable locally, so a
* single-member commit also cannot advance over a not-yet-synced entry. */
if (ce->n_peers <= 1) {
return cap;
}
@ -184,6 +196,37 @@ static void wait_deadline(struct timespec* dl, uint32_t timeout_ms)
}
}
/* Durable-advance wake (ha-commit-durability task 2.2): invoked by the persist
* group-commit flusher AFTER it advances durable_lsn and AFTER it releases
* p->lock. Now that the commit majority caps self at durable_lsn, an advance of
* durable_lsn can newly satisfy a pending entry's majority even when no standby
* ACK arrived — re-evaluate and wake blocked COMMIT_MODE_WAIT callers so they
* complete promptly instead of stalling until their timeout (the lost-wakeup
* window: all standby ACKs landed before the local fdatasync finished, so
* commit_on_ack already fired and nothing else would re-check).
*
* LOCK ORDERING: this runs on the flusher thread with NO persist lock held; it
* takes only ce->lock. The commit side never takes p->lock while holding
* ce->lock, so the only edge is p->lock → ce->lock (cycle-free, deadlock-free).
* ctx is the commit_engine_t registered in commit_engine_init. */
static void commit_on_durable_advance(void* ctx)
{
commit_engine_t* ce = ctx;
if (ce == NULL || ce->p == NULL) {
return;
}
pthread_mutex_lock(&ce->lock);
commit_advance_locked(ce); /* re-derives majority with the new durable cap;
* broadcasts ce->cond on any advance */
/* Broadcast even when committed_lsn did not advance: the durable watermark
* moved, so a waiter's wait_done predicate (which the durable cap feeds via
* commit_advance_locked) must be re-checked. commit_advance_locked only
* broadcasts on a committed_lsn change, so cover the no-change case here. */
pthread_cond_broadcast(&ce->cond);
pthread_mutex_unlock(&ce->lock);
}
/* ============================================================
* Lifecycle
* ============================================================ */
@ -201,6 +244,12 @@ int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcp
ce->inflight_waits = 0;
ce->writable_lsn = 0; /* no own-term gate until set by promotion */
memset(ce->scratch, 0, sizeof(ce->scratch));
/* Register the durable-advance wake so the flusher can re-trigger commit
* evaluation when durable_lsn moves (task 2.2). Persist owns the hook slot;
* we only register a function + ctx, preserving the commit→persist (never
* the reverse) dependency direction. */
persist_set_durable_cb(p, commit_on_durable_advance, ce);
return 0;
}
@ -209,6 +258,14 @@ void commit_engine_fini(commit_engine_t* ce)
if (ce == NULL) {
return;
}
/* Unregister the durable-advance wake BEFORE tearing down the mutex/cond:
* otherwise the flusher could fire commit_on_durable_advance against a
* destroyed engine (use-after-free / ASAN). Callers are expected to have
* stopped the flusher (persist_stop) first, but unhooking is defensive and
* cheap. After this, persist holds no pointer to ce. */
if (ce->p != NULL) {
persist_set_durable_cb(ce->p, NULL, NULL);
}
pthread_cond_destroy(&ce->cond);
pthread_mutex_destroy(&ce->lock);
ce->p = NULL;