From 08f73598a5bb3698fa0e0fa0bc4b0037554a9a98 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Thu, 7 May 2026 00:29:43 +0300 Subject: [PATCH] =?UTF-8?q?Fix=20WAL=20crash=20recovery=20=E2=80=94=20data?= =?UTF-8?q?=20survives=20power=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wal.nim: open existing WAL with fmAppend instead of fmWrite (was truncating!) - lsm.nim: replay WAL entries into memTable on startup before accepting writes - Fixes real data loss on crash — previously WAL was wiped on every restart --- src/barabadb/storage/lsm.nim | 40 +++++++++++++++++++++++++++++++++++- src/barabadb/storage/wal.nim | 10 +++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/barabadb/storage/lsm.nim b/src/barabadb/storage/lsm.nim index 268ee48..0e09186 100644 --- a/src/barabadb/storage/lsm.nim +++ b/src/barabadb/storage/lsm.nim @@ -10,6 +10,7 @@ import std/locks import bloom import wal import mmap +import streams const SSTableMagic* = 0x53535442'u32 # "SSTB" @@ -323,7 +324,44 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree = result.memMaxSize = memMaxSize result.currentSeq = 0 result.nextSSTableId = nextId - discard + + # WAL crash recovery — replay unflushed entries into memTable + let walPath = dir / "wal" / "wal.log" + if fileExists(walPath): + try: + let stream = newFileStream(walPath, fmRead) + if stream != nil: + var magic: uint32 = 0 + var version: uint32 = 0 + if stream.readData(addr magic, 4) == 4 and magic == WALMagic: + if stream.readData(addr version, 4) == 4: + 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 + 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 + var value = newSeq[byte](valLen.int) + if valLen > 0: + if stream.readData(addr value[0], valLen.int) != valLen.int: break + case WalEntryKind(kind) + of wekPut: + discard result.memTable.put(key, value, timestamp) + of wekDelete: + discard result.memTable.put(key, @[], timestamp) # tombstone + of wekCommit: + discard + of wekCheckpoint: + discard + stream.close() + except: + discard proc put*(db: LSMTree, key: string, value: seq[byte]) = acquire(db.lock) diff --git a/src/barabadb/storage/wal.nim b/src/barabadb/storage/wal.nim index a70fef2..fa8d48e 100644 --- a/src/barabadb/storage/wal.nim +++ b/src/barabadb/storage/wal.nim @@ -28,12 +28,14 @@ type proc newWriteAheadLog*(dir: string, syncOnWrite: bool = true): WriteAheadLog = createDir(dir) let path = dir / "wal.log" - let stream = newFileStream(path, fmWrite) + let exists = fileExists(path) + let stream = if exists: newFileStream(path, fmAppend) else: newFileStream(path, fmWrite) if stream == nil: raise newException(IOError, "Cannot open WAL: " & path) - stream.write(WALMagic) - stream.write(WALVersion) - stream.flush() + if not exists: + stream.write(WALMagic) + stream.write(WALVersion) + stream.flush() WriteAheadLog(path: path, stream: stream, entryCount: 0, syncOnWrite: syncOnWrite) proc writeEntry*(wal: var WriteAheadLog, entry: WalEntry) =