feat(ha-repl-hardening): wire OPCode_walCatchupEntry standby handler (task 2.1)

Replace stub with ha_apply_wal_catchup_entry() calling apply_wal_entry().
CAS-advance applied_lsn on success; log ERROR and skip on failure.
Standby stays in CTRL_STANDBY throughout. 3 new unit tests.
This commit is contained in:
Rongsong Shen 2026-06-04 15:05:52 +00:00
parent e3ca775f0d
commit 647b88ce57
5 changed files with 251 additions and 1 deletions

View file

@ -803,6 +803,65 @@ void ha_fini(ha_state_t *ha)
}
}
/* ── Task 2.1: 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.
*
* On success (rc == 0): atomically advances ha->applied_lsn to entry->lsn+1
* so the next heartbeat reply's stateReport reflects the latest state.
*
* 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).
*
* 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).
*
* Exported under XCE_TESTING so unit tests can call it directly without
* constructing a full cluster_t or endpoint_t.
*/
int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
wal_catchup_entry_t *entry)
{
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) {
if (atomic_compare_exchange_weak_explicit(
&ha->applied_lsn, &old_lsn, new_lsn,
memory_order_acq_rel, memory_order_acquire))
break;
}
return 0;
}
/* ── Dispatch gate (req 1.3, 1.4) ──────────────────────────────────────────
*
* Called from myreq_handler() in cluster.c before the opcode switch.