feat(ha-repl-hardening): add batch WAL catch-up on standby side (task 2.2)

Extend ha_apply_wal_catchup_entry with batch branch:
- Check n_entries > 0, iterate entries[] list in order
- Apply each via apply_wal_entry(); skip NULL/errors without crash
- Advance applied_lsn once after full batch (not per-entry)
- n_entries == 0 falls through to task 2.1 single-entry path
4 new unit tests covering full batch, partial batch, error skip, fallback
This commit is contained in:
Rongsong Shen 2026-06-04 15:38:20 +00:00
parent 647b88ce57
commit 6bbcff62ca
3 changed files with 295 additions and 41 deletions

View file

@ -803,53 +803,38 @@ void ha_fini(ha_state_t *ha)
}
}
/* ── Task 2.1: WAL catch-up entry application (standby side) ────────────────
/* ── Task 2.1/2.2: WAL catch-up entry application (standby side) ─────────────
*
* ha_apply_wal_catchup_entry: apply a single WAL catch-up entry received from
* the primary to local persistent storage and advance applied_lsn on success.
* ha_apply_wal_catchup_entry: apply WAL catch-up entries received from the
* primary to local persistent storage and advance applied_lsn on success.
*
* On success (rc == 0): atomically advances ha->applied_lsn to entry->lsn+1
* so the next heartbeat reply's stateReport reflects the latest state.
* Two modes (Req 5.3, 5.5):
*
* On error (rc < 0): logs ERROR and returns the error code. The dispatch
* handler in cluster.c ignores the error and continues, satisfying Req 3.3
* (corrupt/disk-error entries are skipped, not crashed).
* Batch mode (entry->n_entries > 0):
* Iterate entry->entries[0..n_entries-1] in order. Apply each sub-entry
* individually via apply_wal_entry(). NULL sub-entries are treated as
* corrupt and skipped (Req 3.3). After ALL sub-entries are processed,
* advance applied_lsn to last-successfully-applied lsn+1 so the next
* heartbeat reply reflects post-batch state (Req 5.3).
*
* applied_lsn is in-memory only (not persisted to disk). On restart the
* standby reports lsn=0 and re-bootstraps via the heartbeat decision loop
* (design §Durability).
* Single-entry mode (entry->n_entries == 0):
* Backward-compatible path from task 2.1. Apply entry->lsn/entry_type
* directly and advance applied_lsn to lsn+1 on success.
*
* Exported under XCE_TESTING so unit tests can call it directly without
* constructing a full cluster_t or endpoint_t.
* In both modes:
* - applied_lsn is advanced atomically and monotonically.
* - On error for any individual entry: log ERROR, skip, continue (Req 3.3).
* - applied_lsn is in-memory only; not persisted to disk (design §Durability).
* - Standby stays in CTRL_STANDBY throughout (Req 3.4).
*
* Returns 0 on success (even if some batch entries were skipped due to errors).
* Returns -EINVAL if ha, p, or entry is NULL.
*/
int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
wal_catchup_entry_t *entry)
/* ha_advance_applied_lsn: atomically advance ha->applied_lsn to new_lsn
* if new_lsn > current value (monotonic). */
static void ha_advance_applied_lsn(ha_state_t *ha, uint64_t new_lsn)
{
if (!ha || !p || !entry)
return -EINVAL;
/* Build a wal_entry_hdr_t from the decoded catch-up entry fields.
* payload_len is 0 when entry->payload is NULL (no payload for this
* entry type, or single-entry path before task 3.4 encodes real data). */
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.lsn = entry->lsn;
hdr.type = (uint32_t)entry->entry_type;
hdr.payload_len = 0;
hdr.crc = 0;
hdr.prev_crc = 0;
int rc = apply_wal_entry(p, &hdr, NULL);
if (rc != 0) {
ERROR("ha_apply_wal_catchup_entry: apply_wal_entry failed for "
"lsn=%" PRIu64 " type=%u rc=%d — skipping entry",
entry->lsn, (unsigned)entry->entry_type, rc);
return rc;
}
/* Advance applied_lsn atomically and monotonically (lsn+1 mirrors
* persist->current_lsn after a successful apply). */
uint64_t new_lsn = entry->lsn + 1;
uint64_t old_lsn = (uint64_t)atomic_load_explicit(&ha->applied_lsn,
memory_order_acquire);
while (new_lsn > old_lsn) {
@ -858,7 +843,78 @@ int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
memory_order_acq_rel, memory_order_acquire))
break;
}
}
/* ha_apply_single_entry: apply one decoded wal_catchup_entry_t sub-entry.
* Returns 0 on success, non-zero on error (caller logs and skips). */
static int ha_apply_single_entry(persist_t *p, wal_catchup_entry_t *e)
{
wal_entry_hdr_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.lsn = e->lsn;
hdr.type = (uint32_t)e->entry_type;
hdr.payload_len = 0;
hdr.crc = 0;
hdr.prev_crc = 0;
return apply_wal_entry(p, &hdr, NULL);
}
int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
wal_catchup_entry_t *entry)
{
if (!ha || !p || !entry)
return -EINVAL;
/* ── Batch mode: entry->n_entries > 0 ─────────────────────────────────── */
if (entry->n_entries > 0) {
/* Apply each sub-entry in LSN order. Track the last successfully
* applied LSN so we can advance applied_lsn after the full batch.
* Req 5.5: applied_lsn advances only after the full batch is written.
* Req 3.3: NULL or failed entries are logged and skipped; no crash. */
uint64_t last_good_lsn = 0;
int had_good = 0;
for (int i = 0; i < entry->n_entries; i++) {
wal_catchup_entry_t *sub = entry->entries[i];
if (!sub) {
ERROR("ha_apply_wal_catchup_entry: NULL sub-entry at "
"batch index %d — skipping", i);
continue;
}
int rc = ha_apply_single_entry(p, sub);
if (rc != 0) {
ERROR("ha_apply_wal_catchup_entry: apply_wal_entry failed "
"for batch index %d lsn=%" PRIu64 " type=%u rc=%d "
"— skipping entry",
i, sub->lsn, (unsigned)sub->entry_type, rc);
continue;
}
last_good_lsn = sub->lsn;
had_good = 1;
}
/* Advance applied_lsn to last_good_lsn+1 after the full batch.
* Only advance if at least one entry was applied successfully. */
if (had_good)
ha_advance_applied_lsn(ha, last_good_lsn + 1);
return 0;
}
/* ── Single-entry mode (task 2.1 backward-compatible path) ────────────── */
int rc = ha_apply_single_entry(p, entry);
if (rc != 0) {
ERROR("ha_apply_wal_catchup_entry: apply_wal_entry failed for "
"lsn=%" PRIu64 " type=%u rc=%d — skipping entry",
entry->lsn, (unsigned)entry->entry_type, rc);
return rc;
}
/* Advance applied_lsn atomically (lsn+1 mirrors persist->current_lsn
* after a successful apply). */
ha_advance_applied_lsn(ha, entry->lsn + 1);
return 0;
}