## Crash Recovery — WAL replay with REDO/UNDO import std/streams import std/os import ../storage/wal import ../storage/lsm type RecoveryState* = enum recScanning recRedoing recUndoing recDone RecoveryResult* = object state*: RecoveryState totalEntries*: int redone*: int undone*: int lastLsn*: uint64 lastTxn*: uint64 applied*: bool RecoveredEntry* = object key*: string value*: seq[byte] lsn*: uint64 txnId*: uint64 isDelete*: bool CrashRecovery* = ref object walDir*: string dataDir*: string entries*: seq[RecoveredEntry] result*: RecoveryResult lastTxnId*: uint64 # tracks commits seen in WAL proc newCrashRecovery*(walDir: string, dataDir: string): CrashRecovery = CrashRecovery( walDir: walDir, dataDir: dataDir, entries: @[], result: RecoveryResult(state: recScanning), ) proc scanWAL*(rec: CrashRecovery): seq[RecoveredEntry] = result = @[] let walPath = rec.walDir / "wal.log" if not fileExists(walPath): return let stream = newFileStream(walPath, fmRead) if stream == nil: return var magic: uint32 = 0 var version: uint32 = 0 if stream.readData(addr magic, 4) != 4: stream.close() return if magic != WALMagic: stream.close() return if stream.readData(addr version, 4) != 4: stream.close() return var txnId: uint64 = 0 var entryCount = 0 const MaxWalRecordField = 64 * 1024 * 1024 # 64 MB while not stream.atEnd(): var kind: uint8 = 0 var timestamp: uint64 = 0 var keyLen: uint32 = 0 var valLen: uint32 = 0 if stream.readData(addr kind, 1) != 1: break if stream.readData(addr timestamp, 8) != 8: break if stream.readData(addr keyLen, 4) != 4: break if keyLen.int > MaxWalRecordField: break if kind < uint8(wekPut) or kind > uint8(wekCommit): break var key = newString(keyLen.int) if keyLen > 0: if stream.readData(addr key[0], keyLen.int) != keyLen.int: break if stream.readData(addr valLen, 4) != 4: break if valLen.int > MaxWalRecordField: break var value = newSeq[byte](valLen.int) if valLen > 0: if stream.readData(addr value[0], valLen.int) != valLen.int: break inc entryCount case WalEntryKind(kind) of wekPut: result.add(RecoveredEntry(key: key, value: value, lsn: uint64(entryCount), txnId: txnId)) of wekDelete: result.add(RecoveredEntry(key: key, value: @[], lsn: uint64(entryCount), txnId: txnId, isDelete: true)) of wekCommit: inc txnId of wekCheckpoint: discard stream.close() rec.lastTxnId = txnId proc analyze*(rec: CrashRecovery): RecoveryResult = rec.entries = rec.scanWAL() if rec.entries.len == 0: rec.result = RecoveryResult(state: recDone, applied: false) return rec.result var lastCommitted = rec.lastTxnId var redoCount = 0 var undoCount = 0 for entry in rec.entries: if entry.txnId < lastCommitted: inc redoCount else: inc undoCount rec.result = RecoveryResult( state: recDone, totalEntries: rec.entries.len, redone: redoCount, undone: undoCount, lastLsn: if rec.entries.len > 0: rec.entries[^1].lsn else: 0, lastTxn: lastCommitted, applied: true, ) return rec.result proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult = let analysis = rec.analyze() if not analysis.applied: return analysis if db == nil: return analysis # REDO: apply committed entries (txnId < lastCommitted) to LSM-Tree var redoCount = 0 var undoCount = 0 for entry in rec.entries: if entry.txnId < analysis.lastTxn: # Committed — redo (bypass WAL to avoid duplicate entries) if entry.isDelete: db.deleteUnsafe(entry.key) else: db.putUnsafe(entry.key, entry.value) inc redoCount else: # Uncommitted — skip (undo) inc undoCount rec.result.redone = redoCount rec.result.undone = undoCount rec.result.state = recDone return rec.result proc totalEntries*(rec: CrashRecovery): int = rec.entries.len proc summary*(rec: CrashRecovery): string = let r = rec.analyze() result = "WAL Recovery Summary:\n" result &= " Total entries: " & $r.totalEntries & "\n" result &= " Redone (committed): " & $r.redone & "\n" result &= " Undone (uncommitted): " & $r.undone & "\n" result &= " Last committed txn: " & $r.lastTxn & "\n" result &= " Recovery complete: " & $r.applied