Files
Baradb/src/barabadb/storage/recovery.nim
T
dimgigov 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
fix: audit batch 2 — semi-sync, DISTINCT, set ops, MERGE, storage hardening
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.
2026-08-02 23:12:47 +03:00

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