e44341e47c
CI / test (push) Has been cancelled
CI / raft-e2e (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
Address 12 deep-audit findings: semi-sync fail-closed on partial ack, COUNT/SUM/AVG(DISTINCT), UNION/INTERSECT/EXCEPT dedup, MERGE THEN DELETE, WAL torn-record recovery, MVCC/checkpoint/flush/compaction/WAL rewrite safety, mmap overflow bounds; remove stray protocol/scram ELF.
181 lines
4.5 KiB
Nim
181 lines
4.5 KiB
Nim
## 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
|