feat(ha-commit-durability): gate legacy sync-append commit store on durable_lsn + commit<=durable invariant (task 2.4)

This commit is contained in:
Rongsong Shen 2026-06-18 07:09:47 +00:00
parent e4c4968331
commit 526bca9679
5 changed files with 250 additions and 4 deletions

View file

@ -1580,6 +1580,42 @@ int persist_recover(persist_t* p)
return 0;
}
/* Advance committed_lsn to `written_lsn` on a majority ACK — but ONLY if that
* entry is already durable on local stable storage (ha-commit-durability task
* 2.4, Req 1.1/1.2). This is the legacy (engine-less / LSN-0) sibling of the
* commit engine's durable self-cap (commit.c majority_candidate_locked): a
* majority ACK no longer implies the local copy is fsync'd, because group-
* commit (task 2.1) deferred the append fsync to the demand-driven flusher. So
* advancing committed_lsn here on the ACK alone would reintroduce the bug-661
* ACK-without-durability hazard — committed could be durable on a single
* member and lost across failover. We gate the store on written_lsn <=
* durable_lsn so committed_lsn ⇒ durable-on-this-member on every path.
* committed_lsn is monotonic, so a max() store; the durable cap can only delay
* the advance until the flusher syncs the entry (the durable-advance commit
* wake re-drives the engine path; the legacy path is re-evaluated on the next
* sync_append). */
static void sync_append_legacy_commit_store(persist_t* p, uint64_t written_lsn)
{
uint64_t durable = atomic_load_explicit(&p->durable_lsn,
memory_order_acquire);
/* DURABILITY PRECONDITION: never advance committed past what is durable
* locally. The LSN-0 first-entry edge stays valid (0 <= 0). */
if (written_lsn > durable)
return;
uint64_t cur = atomic_load_explicit(&p->committed_lsn,
memory_order_acquire);
if (written_lsn <= cur)
return; /* monotonic: committed_lsn never regresses */
atomic_store_explicit(&p->committed_lsn, written_lsn, memory_order_release);
#ifdef XCE_TESTING
/* INVARIANT (debug guard): committed_lsn must never exceed durable_lsn on
* any commit advance. A future caller reintroducing ACK-without-durability
* trips this. */
assert(atomic_load_explicit(&p->committed_lsn, memory_order_acquire)
<= atomic_load_explicit(&p->durable_lsn, memory_order_acquire));
#endif
}
int persist_wal_sync_append(persist_t* p, uint32_t type,
const void* payload, uint32_t len,
int* sync_rc_out)
@ -1592,7 +1628,12 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
}
/* 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.
* persist_wal_async appends to the page cache and signals the demand-driven
* flusher; it no longer fsyncs inline (group-commit, task 2.1). Durability
* is therefore established by the flusher advancing durable_lsn, and a
* commit position advances only once the entry is durable locally — the
* engine path via the durable self-cap (commit.c), the legacy path below
* via sync_append_legacy_commit_store's written_lsn <= durable_lsn gate.
* We use current_lsn - 1 because persist_wal_async already incremented it. */
uint64_t written_lsn;
pthread_mutex_lock(&p->lock);
@ -1639,8 +1680,10 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
p->config.sync_timeout_ms,
(uint8_t)OPCode_walAck);
if (sync_err == 0)
atomic_store_explicit(&p->committed_lsn, written_lsn,
memory_order_release);
/* Durable-gated commit advance (task 2.4): advance committed_lsn
* only if written_lsn is durable locally — never on the ACK
* alone (group-commit removed the inline append fsync). */
sync_append_legacy_commit_store(p, written_lsn);
else
CTRL_METRIC_INC(ctrl_repl_degraded_count);
}
@ -1662,6 +1705,19 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
return 0;
}
#ifdef XCE_TESTING
/* Test seam (task 2.4): invoke the EXACT durable-gated commit store that the
* legacy sync_append branch uses on a majority ACK, so a test can prove the
* ACK-without-durability hazard is closed without standing up the replication
* wire. Pre-fix this branch stored committed_lsn = written_lsn unconditionally
* (over-advancing past durable_lsn). */
void persist_test_sync_append_legacy_commit_store(persist_t* p,
uint64_t written_lsn)
{
sync_append_legacy_commit_store(p, written_lsn);
}
#endif /* XCE_TESTING */
void persist_log_entry_metadata(const wal_entry_hdr_t* hdr)
{
TRACE("WAL entry: type=%u lsn=%lu term=%lu payload_len=%u crc=%08x",