myxce/.claude/skills/xce-test-ha/wal-layout-check.py
Rongsong Shen 5cf923350c
test(soak): WAL segment-layout gate and --truncated-fence scenario
- wal-layout-check.py: every WAL segment must hold one contiguous LSN run
  starting at the LSN in its name (bug-1920 signature); the sequential,
  chaotic and truncated-fence modes fail on a misfiled segment.
- --truncated-fence: breaks ctrl-1's WAL tail CRC so it boots
  RECOVER_TRUNCATED, makes it win an election (promotion REFUSED), then
  requires the fence to be healed and released after catch-up and the
  node to lead with zero job loss (bug-1919 lifecycle, proven from logs).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 03:03:09 +00:00

72 lines
2.4 KiB
Python
Executable file

#!/usr/bin/env python3
"""wal-layout-check.py -- check every controller's WAL segment layout (bug-1920).
Each WAL segment file is named seg-<first LSN in hex>.wal and must hold one
contiguous LSN run starting at that LSN. The catch-up streamer
(ha_stream_wal_to_standby) orders segments by filename and skips a segment
whose successor starts at or below the standby's position, so an LSN filed
under the wrong segment name (bug-1920: a replicated write before boot
recovery truncated and reused seg-...0001) is never streamed and a standby
that needs it stalls.
Usage: wal-layout-check.py <deploy-top> (scans <top>/controller/*/data/wal)
Exit 0 when every segment is consistent, 1 otherwise (one line per problem).
"""
import glob
import os
import struct
import sys
HDR = struct.Struct("<QQQIIII") # term, lsn, timestamp, type, payload_len, crc, prev_crc
def scan_segment(path):
"""Return (first_lsn, last_lsn, n_entries, n_jumps)."""
first = last = None
n = jumps = 0
with open(path, "rb") as f:
data = f.read()
off = 0
while off + HDR.size <= len(data):
_term, lsn, _ts, typ, plen, _crc, _pcrc = HDR.unpack_from(data, off)
if lsn == 0 and typ == 0 and plen == 0:
break # pre-allocated zero tail
if first is None:
first = lsn
elif lsn != last + 1:
jumps += 1
last = lsn
n += 1
off += HDR.size + plen
return first, last, n, jumps
def main():
if len(sys.argv) != 2:
print(__doc__.strip().splitlines()[-2], file=sys.stderr)
return 2
top = sys.argv[1]
problems = []
nseg = 0
for path in sorted(glob.glob(os.path.join(top, "controller", "*", "data", "wal", "seg-*.wal"))):
name = os.path.basename(path)
try:
start = int(name[4:-4], 16)
except ValueError:
continue
nseg += 1
first, last, n, jumps = scan_segment(path)
if n == 0:
continue
node = path.split(os.sep)[-4]
if first != start or jumps:
problems.append(f"{node}/{name}: entries {first}..{last} ({n}, {jumps} LSN jump(s))"
f" -- expected a contiguous run starting at {start}")
for p in problems:
print(f"WAL layout: {p}")
print(f"WAL layout: {nseg} segment(s) checked, {len(problems)} problem(s)")
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main())