- 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>
72 lines
2.4 KiB
Python
Executable file
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())
|