Exercise 3: Recovery Interrupted Mid-Replay, Then Re-Run — Possible Solution ==================================================================== THE TEST ------------------------------ heap = HeapFile(heap_path) wal = WAL(wal_path) pn1 = heap.allocate_page() pn2 = heap.allocate_page() p1 = Page(); p1.add_record(b'FIRST-PAGE-DATA') p2 = Page(); p2.add_record(b'SECOND-PAGE-DATA') wal.append(pn1, bytes(p1.data)) wal.append(pn2, bytes(p2.data)) try: recover(heap, wal_path, crash_after_n=1) # crash after record 0, before record 1 except RuntimeError: pass # simulated interruption of recovery itself mid1 = heap.read_page(pn1) mid2 = heap.read_page(pn2) wal_still_there = read_all_records(wal_path) n2 = recover(heap, wal_path) # re-run recovery from scratch, no crash this time final1 = heap.read_page(pn1) final2 = heap.read_page(pn2) RESULT ------------------------------ after interrupted recovery -- page 1 applied: True after interrupted recovery -- page 2 applied: False WAL still has both records (recover() never truncates anything): 2 re-running recover() from scratch applies: 2 records; both pages now correct: True WHY THE PLAIN recover() SURVIVES ITS OWN INTERRUPTION ------------------------------ Unlike recover_naive_truncate_first() from the chapter's own Finding 3, the plain recover() function used throughout this chapter never calls open(wal_path, 'wb').close() at all -- it only reads records and writes pages. There is nothing in it capable of destroying the log's own contents, no matter where an interruption happens to land. After the simulated crash, read_all_records(wal_path) still returns both original records untouched, exactly as they were logged. WHY SIMPLY RE-RUNNING recover() IS ENOUGH ------------------------------ Because every record in the WAL is still present and verifiable, calling recover() again from scratch replays BOTH records -- including page 1's own record, which was already correctly applied during the interrupted first attempt. Per Finding 2, reapplying an already-correct record is a harmless no-op, so this second call doesn't need to know it's a "resume" rather than a fresh run -- it's not, structurally; it's exactly the same function, called the exact same way, and it happens to produce the correct final result because of idempotent replay, not because of any resume-specific logic. WHY THIS IS THE EXERCISE'S OWN POINT ------------------------------ This is precisely what makes recover_safe_truncate_last() (the chapter's own fix) correct: since PLAIN recovery is already safely re-runnable no matter where it's interrupted, the ONLY genuinely unsafe operation anywhere in the whole routine is truncating the log -- and that's exactly why the fix is "move truncation to the very end," not "add crash-safety logic to the replay loop itself." The replay loop was never the problem. WHY THIS WORKS AS AN ANSWER ------------------------------ Deliberately interrupting recover() itself, confirming the WAL survives that interruption completely intact, and confirming a second, ordinary call finishes the job correctly demonstrates -- with a real, reproduced crash rather than an assumption -- that recover() requires no special resume behavior of its own, which is the exact property Finding 3's fix depends on.