Exercise 2: next_lsn After Reopening a Truncated WAL — Possible Solution ==================================================================== THE TEST ------------------------------ wal_a = WAL(wal_path) pageA = Page(); pageA.add_record(b'AAA') pageB = Page(); pageB.add_record(b'BBB') lsn_a = wal_a.append(1, bytes(pageA.data)) # LSN 1 lsn_b = wal_a.append(2, bytes(pageB.data)) # LSN 2 # simulate a crash: truncate the file so only record 1's own bytes remain size_after_record1 = 8 + (12 + PAGE_SIZE) # header + payload, one record with open(wal_path, 'r+b') as f: f.truncate(size_after_record1) wal_b = WAL(wal_path) # a fresh WAL object -- simulates reopening after a restart print(wal_b.next_lsn) RESULT ------------------------------ wrote 2 records, LSNs: 1 2, full file size: 552 after truncating to just record 1 and reopening: next_lsn = 2 next_lsn correctly comes back as 2 -- one more than record 1's own LSN of 1 -- not 3, and not some other value that might have been parsed out of record 2's now-discarded bytes. WHY THIS WORKS CORRECTLY WITHOUT ANY SPECIAL-CASE CODE ------------------------------ WAL.__init__ computes next_lsn like this: existing = read_all_records(path) self.next_lsn = (max(r['lsn'] for r in existing) + 1) if existing else 1 read_all_records() is the SAME real, checksum-verified, bounds- checked reader used everywhere else in this chapter -- it has no special "am I being called during startup" mode. Truncating the file to exactly the byte boundary after record 1 leaves record 2 with zero bytes on disk at all (rather than a torn fragment of it, as Chapter 2's own Finding 2 demonstrated) -- read_all_records() simply never sees any bytes claiming to be a second record, so it returns a list containing only record 1. next_lsn is then computed from whatever records genuinely, verifiably exist -- which is exactly one record, with LSN 1 -- giving next_lsn = 2. WHY THIS MATTERS ------------------------------ If next_lsn were instead tracked as some separately-stored counter (written to its own file, or kept as the last value used before a crash), reopening the WAL after a crash could produce a next_lsn that's inconsistent with what the log file itself can actually prove happened -- exactly the kind of separately-maintained state that can drift out of sync, echoing Chapter 1's own index-vs-data consistency bug. Deriving next_lsn FROM the verified, checksummed records themselves, every time the WAL is opened, means there is only ever one source of truth: what the log file can actually prove. WHY THIS WORKS AS AN ANSWER ------------------------------ Confirming next_lsn recovers correctly after a real truncation -- without any code written specifically to handle "what if I reopen after a crash" -- shows the reader's own general-purpose validation logic (used for every read, not just startup) is what makes recovery of this specific piece of state trustworthy, rather than needing a separate, special-cased recovery path of its own.