test(ha-term-safety): cross-member walcompare divergence tool (task 5.3)

Diffs member WALs by (lsn -> term, pcrc) over the committed prefix, exits non-zero
on any committed-prefix divergence with the diverging LSN + per-member entry
identities. Reuses the canonical waldump decoder via a new per-entry visitor seam
(no second WAL parser). Detects same-LSN/diff-term and same-LSN/diff-content
divergences (the bug-851 class) that an LSN-range check would mask.
This commit is contained in:
Rongsong Shen 2026-06-22 13:05:23 +00:00
parent d39500ba48
commit a166ecb45b
9 changed files with 842 additions and 5 deletions

View file

@ -0,0 +1,64 @@
#pragma once
/*
* walcompare.h — cross-member WAL committed-prefix divergence detector
* (ha-term-safety R6.1/R6.2/R6.3).
*
* Reuses the canonical waldump.c WAL decoder (via waldump_path_visit) to index
* each member's log by `(lsn -> term, pcrc)`, then asserts that every member's
* COMMITTED prefix is identical by `(term, pcrc)` — not merely by LSN range.
*
* The decisive signal for bug-851: two members can report the same committed
* LSN over a contiguous, CRC-intact WAL yet hold DIFFERENT entries at the same
* LSN (same lsn, different term and/or payload). Comparing LSN ranges alone
* masks that; comparing `(term, pcrc)` per LSN surfaces it (R6.2).
*
* Strictly read-only — opens member WAL dirs O_RDONLY, mutates nothing. Safe to
* run against a live cluster's data dirs.
*/
#include <stdint.h>
#include <stdio.h>
/* A single member's WAL source and its committed boundary. */
typedef struct {
const char* path; /* WAL dir / data dir / seg-*.wal (waldump rules) */
uint64_t committed_lsn; /* highest committed LSN for this member; 0 =
* "derive" (use the shared boundary, see below) */
} walcompare_member_t;
/* Identity of the first divergent entry, filled on a WALCMP_DIVERGENT result. */
typedef struct {
uint64_t lsn; /* the diverging LSN within the committed prefix */
int n_members; /* members compared at this LSN */
uint64_t member_term[16]; /* per-member matched term at lsn */
uint32_t member_pcrc[16]; /* per-member payload CRC at lsn */
int member_present[16]; /* 1 if the member holds this LSN, else 0 */
} walcompare_divergence_t;
/* Result codes. */
enum {
WALCMP_OK = 0, /* all members' committed prefixes are identical */
WALCMP_DIVERGENT = 1, /* a committed-prefix divergence was found */
/* negative values are -errno (a load/usage error: missing WAL, bad args) */
};
/* Compare `n` members' committed WAL prefixes by `(lsn -> term, pcrc)`.
*
* Committed boundary: each member with `committed_lsn == 0` adopts the SHARED
* boundary = the minimum recovered tip (highest CRC-intact LSN) across all
* members, i.e. the largest prefix every member can be expected to hold. A
* member with an explicit `committed_lsn` uses that. An LSN in `[1..boundary]`
* is compared only against the members whose own boundary covers it; a member
* that is legitimately AHEAD (longer log) or behind the shared boundary is not
* flagged for the uncommitted tail it doesn't share (R6.1 is the committed
* prefix, not the full log).
*
* Legacy-zero term (R1.8) is mapped to WAL_LEGACY_TERM for matching, so two
* pre-feature entries are not spuriously divergent.
*
* On the FIRST divergent LSN, fills `*div` (when non-NULL) and prints a report
* to `out`, then returns WALCMP_DIVERGENT. Returns WALCMP_OK on identical
* committed prefixes, or a negative errno on a load/usage error. `out` may be
* NULL to suppress the report.
*/
int walcompare(const walcompare_member_t* members, int n, FILE* out,
walcompare_divergence_t* div);