diff --git a/BUG_AUDIT.md b/BUG_AUDIT.md new file mode 100644 index 0000000..1dd972a --- /dev/null +++ b/BUG_AUDIT.md @@ -0,0 +1,196 @@ +# BaraDB — Ревизия за бъгове и план за подобрения + +> Дата: 2026-05-12 +> Статус: В процес на поправяне + +## Обобщение + +| Категория | Брой | Статус | +|-----------|------|--------| +| 🔴 Критични | 9 | Незабавно поправяне | +| 🟠 Високи | 7 | Поправяне преди production | +| 🟡 Средни | 12 | Планирано поправяне | +| 🟢 Конфигурационни | 4 | Лесни за поправяне | + +**Общ брой тестове:** 283 — всички минават, но тестовото покритие не покрива критични edge cases. + +--- + +## 🔴 Критични бъгове + +### 1. MVCC — Aborted транзакции стават видими +**Файл:** `src/barabadb/core/mvcc.nim` +**Проблем:** `abortTxn` изтрива транзакцията от `activeTxns`. В `isVisible` проверката е: +```nim +if creator in tm.activeTxns and tm.activeTxns[creator].state != tsCommitted: + return false +``` +Ако транзакцията е aborted, тя вече не е в `activeTxns`, затова `isVisible` връща `true` и нейните писания стават видими за всички бъдещи четения. + +**Fix:** Добави `abortedTxns` множество или проверявай `globalVersions` за състоянието на създателя. + +### 2. LSM-Tree — Загуба на данни при immutable memtable overwrite +**Файл:** `src/barabadb/storage/lsm.nim` +**Проблем:** Когато `memTable` се напълни: +```nim +db.immutableMem = db.memTable +db.memTable = newMemTable(db.memMaxSize) +``` +Ако втори `put` дойде преди `flush()` да е извикан, `immutableMem` се презаписва и данните от първия overflow се губят завинаги. + +**Fix:** Проверявай `if db.immutableMem.len > 0: triggerFlush()` преди да присвояваш нова immutable memtable, или използвай опашка. + +### 3. LSM-Tree — Счупена SSTable търсачка +**Файл:** `src/barabadb/storage/lsm.nim` +**Проблем:** `newLSMTree` сортира SSTables по `minKey`. `getUnsafe` търси `countdown(high, low)`, приемайки че по-висок индекс = по-нова таблица. След сортиране по `minKey` това вече не е вярно — стара SSTable може да маскира нова. + +**Fix:** Сортирай по `maxTimestamp` или `id`, не по `minKey`. + +### 4. Auth — JWT подписът е тривиално forgeable +**Файл:** `src/barabadb/protocol/auth.nim` +**Проблем:** `simpleHash()` е djb2-подобен hash, **не HMAC-SHA256**. Токените могат да бъдат фалшифицирани за под 1 секунда. + +```nim +proc simpleHash(data: string, key: string): string = + var h: uint64 = 5381 + for ch in prefix: + h = ((h shl 5) + h) + uint64(ord(ch)) +``` + +**Fix:** Замени с `std/sha256` или `hmac` от `checksums` пакета. + +### 5. Auth — SCRAM-SHA-256 е фалшив +**Файл:** `src/barabadb/protocol/auth.nim` +**Проблем:** Няма challenge-response, salt, iteration count. Сравнява директно `stored == clientHash`. Еквивалентно на plaintext съхранение. + +**Fix:** Имплементирай истински SCRAM-SHA-256 или премахни го. + +### 6. Recovery — `summary()` мутира базата данни +**Файл:** `src/barabadb/storage/recovery.nim` +**Проблем:** `summary()` извиква `recover()`, който реплейва WAL entries в LSMTree. Простото извикване на `summary` променя данните и пише нови WAL entries. + +**Fix:** Раздели `analyze()` (read-only) от `recover()` (mutating). `summary()` трябва да ползва само `analyze()`. + +### 7. DistTxn — Rollback след commit attempt нарушава atomicity +**Файл:** `src/barabadb/core/disttxn.nim` +**Проблем:** В `commit()`, ако някои participants не acknowledge, coordinator се опитва да rollback-не nodes, които вече са върнали `committed = true`. Веднъж commit-нато, не може да се rollback-ва. + +**Fix:** След първия successful commit на participant, не опитвай rollback. Ползвай heuristic recovery или блокирай докато всички acknowledge-нат. + +### 8. Raft — Majority calculation bug за четен брой нодове +**Файл:** `src/barabadb/core/raft.nim` +**Проблем:** `handleAppendReply` използва `matchIndices.len div 2` за median. За 4 нода това дава индекс 2, т.е. искат се само 2 нода, но Raft изисква strict majority (3). + +**Fix:** `matchIndices.len div 2 + 1` или `(matchIndices.len + 1) div 2`. + +### 9. Query — `EXISTS` подзаявки винаги връщат false +**Файл:** `src/barabadb/query/executor.nim` +**Проблем:** `evalExpr` за `irekExists` хардкодира `return "false"`. + +**Fix:** Изпълни подзаявката и провери дали връща редове. + +--- + +## 🟠 Високо приоритетни бъгове + +| # | Модул | Проблем | Последствие | +|---|-------|---------|-------------| +| 10 | `storage/wal.nim` | `sync()` прави само `stream.flush()`, не `fsync` | Загуба на данни при power loss | +| 11 | `protocol/ssl.nim` | Command injection в `generateSelfSignedCert` — конкатенира пътища в shell | RCE ако `BARADB_CERT_FILE` е контролиран от attacker | +| 12 | `protocol/wire.nim` | `readString` заделя `newString(len)` от wire без лимит | OOM/DoS с `len = 0xFFFFFFFF` | +| 13 | `protocol/wire.nim` | Няма bounds checking при deserialization | Out-of-bounds reads от malformed messages | +| 14 | `query/executor.nim` | `exprToSql` не escape-ва quotes в string literals | SQL injection в views и schema DDL | +| 15 | `query/executor.nim` | `irLike`/`irILike` превръща `%`→`.*` без escaping на regex metachars | ReDoS / Catastrophic backtracking | +| 16 | `query/executor.nim` | Stale BTree indexes на UPDATE/DELETE — не изтрива стари индекси | Queries връщат изтрити/обновени редове | + +--- + +## 🟡 Средно приоритетни бъгове + +| # | Модул | Проблем | +|---|-------|---------| +| 17 | `query/lexer.nim` | `true`/`false` се tokenizewat като `tkTrue`/`tkFalse`, но parser очаква `tkBoolLit` → `SELECT true` става `nkNullLit` | +| 18 | `query/lexer.nim` | `1.2.3` се приема като валиден `tkFloatLit` | +| 19 | `query/lexer.nim` | Незатворени block comments (`/*` без `*/`) не raise-ват error | +| 20 | `query/executor.nim` | Aggregate + `*` на empty result access-ва `sourceRows[0]` без проверка → crash | +| 21 | `query/ir.nim` | Unary minus (`-5`) се map-ва към boolean NOT вместо arithmetic negation | +| 22 | `query/udf.nim` | Non-aggregate UDFs в expression се treat-ват като NULL literals | +| 23 | `vector/engine.nim` | Dimension mismatch ползва `min(a.len, b.len)` вместо error | +| 24 | `vector/engine.nim` | Няма locking — concurrent insert + search корумпират HNSW | +| 25 | `fts/engine.nim` | Tokenize мангира UTF-8 (iterates by byte, not grapheme) | +| 26 | `graph/engine.nim` | `addEdge` без проверка дали node съществува → `KeyError` | +| 27 | `core/raft.nim` | Няма disk persistence — `currentTerm`, `votedFor`, log са in-memory | +| 28 | `core/server.nim` | `DISTTXN`/`REP` handlers чакат `recv()` без timeout → hang от malicious peer | + +--- + +## 🔧 Конфигурационни проблеми + +| # | Проблем | Файл | Fix | +|---|---------|------|-----| +| 29 | `nimble build` fail-ва без `-d:ssl`, въпреки `switch("define", "ssl")` в `.nimble` | `baradadb.nimble` | Добави `switch("define", "ssl")` в `nim.cfg` или използвай `before build:` hook | +| 30 | `bench` task сочи към `benchmarks/bench_storage.nim` който не съществува | `baradadb.nimble` | Промени на `benchmarks/bench_all.nim` | +| 31 | CI build benchmarks без `-d:ssl`, но `bench_all.nim` може да изисква SSL | `.github/workflows/ci.yml` | Добави `-d:ssl` | +| 32 | `threadpool` е deprecated (Nim 2.2) | `src/baradadb.nim` | Мигрирай към `malebolgia`/`weave`/`taskpools` | + +--- + +## 📋 План за подобрения + +### Незабавни действия (Sprint 0 — 1-2 седмици) + +- [ ] Поправи MVCC isVisible (aborted txns) +- [ ] Поправи LSM-Tree immutable memtable overwrite +- [ ] Поправи LSM-Tree SSTable search order +- [ ] Замени simpleHash с HMAC-SHA256 в auth +- [ ] Поправи recovery.summary() да не мутира +- [ ] Поправи Raft majority calculation +- [ ] Поправи WAL sync() да прави fsync +- [ ] Поправи nimble build (-d:ssl) + +### Краткосрочни (Sprint 1-2 — 1 месец) + +- [ ] Имплементирай истински SCRAM-SHA-256 или го премахни +- [ ] Добави bounds checking в wire protocol +- [ ] Escape-вай string literals в exprToSql +- [ ] Поправи EXISTS subqueries +- [ ] Поправи stale BTree indexes на UPDATE/DELETE +- [ ] Добави timeouts на всички network reads +- [ ] Поправи boolean literal parsing +- [ ] Добави locking в HNSW/vector engine + +### Средносрочни (1-2 месеца) + +- [ ] Raft disk persistence (write-ahead log за Raft state) +- [ ] Command injection fix в SSL модул +- [ ] ReDoS fix в LIKE (escape regex metachars или не използвай regex) +- [ ] UTF-8 tokenization в FTS +- [ ] Memory management: unbounded version chains в MVCC +- [ ] Rate limiter: enforce globalRate, fix memory leaks +- [ ] Property-based тестове за LSM-Tree edge cases + +### Дългосрочни (Formal Verification) + +| Задача | Приоритет | +|--------|-----------| +| `backup.tla` — restore atomicity | Висок | +| `recovery.tla` — WAL replay correctness | Висок | +| `crossmodal.tla` — cross-modal consistency | Среден | +| Symmetry reduction в `.cfg` файловете | Нисък (performance) | + +--- + +## 🎯 Ключеви метрики за проследяване + +| Метрика | Цел | +|---------|-----| +| Тестово покритие | > 80% (сега вероятно < 50% за edge cases) | +| Fuzz тестове | Поне 1 fuzz target per storage engine | +| Security audit | Поправи всички 🔴 и 🟠 issues | +| Benchmark consistency | `nimble bench` да работи out-of-the-box | + +--- + +## Заключение + +BaraDB има солидна архитектура и много функционалности, но има **критични бъгове в core storage, MVCC, auth и distributed транзакциите**, които я правят **неподходяща за production** в момента. Препоръчвам фокусиран sprint върху критичните issues преди добавяне на нови функции. diff --git a/baradadb.nimble b/baradadb.nimble index ac2b927..69c90a3 100644 --- a/baradadb.nimble +++ b/baradadb.nimble @@ -7,8 +7,6 @@ srcDir = "src" bin = @["baradadb"] binDir = "build" -switch("define", "ssl") - # Dependencies requires "nim >= 2.2.0" requires "https://github.com/katehonz/hunos >= 1.2.0" @@ -26,4 +24,4 @@ task test, "Run all tests": exec "nim c --path:src -d:ssl -r tests/test_all.nim" task bench, "Run benchmarks": - exec "nim c --path:src -d:ssl -d:release -r benchmarks/bench_storage.nim" + exec "nim c --path:src -d:ssl -d:release -r benchmarks/bench_all.nim" diff --git a/nim.cfg b/nim.cfg new file mode 100644 index 0000000..521e21d --- /dev/null +++ b/nim.cfg @@ -0,0 +1 @@ +-d:ssl diff --git a/src/barabadb/core/mvcc.nim b/src/barabadb/core/mvcc.nim index 8bdeb4d..e0d5412 100644 --- a/src/barabadb/core/mvcc.nim +++ b/src/barabadb/core/mvcc.nim @@ -43,6 +43,8 @@ type nextLsn: uint64 activeTxns: Table[TxnId, Transaction] committedTxns: seq[TxnId] + committedTxnsSet: HashSet[TxnId] + abortedTxns: HashSet[TxnId] globalVersions*: Table[string, seq[VersionedRecord]] txnTimeoutMs: int64 deadlockDetector*: DeadlockDetector @@ -59,6 +61,8 @@ proc newTxnManager*(txnTimeoutMs: int64 = 30000): TxnManager = result.nextLsn = 1 result.activeTxns = initTable[TxnId, Transaction]() result.committedTxns = @[] + result.committedTxnsSet = initHashSet[TxnId]() + result.abortedTxns = initHashSet[TxnId]() result.globalVersions = initTable[string, seq[VersionedRecord]]() result.txnTimeoutMs = txnTimeoutMs result.deadlockDetector = newDeadlockDetector() @@ -112,8 +116,13 @@ proc isVisible(tm: TxnManager, txn: Transaction, version: VersionedRecord): bool return false if creator in txn.snapshotTxns: return false + if creator in tm.abortedTxns: + return false if creator in tm.activeTxns and tm.activeTxns[creator].state != tsCommitted: return false + # If creator is not active and not committed, it must be aborted + if creator notin tm.activeTxns and creator notin tm.committedTxnsSet: + return false # Deleter must not be a visible committed txn if deleter != TxnId(0): @@ -246,6 +255,7 @@ proc commit*(tm: TxnManager, txn: Transaction): bool = txn.state = tsCommitted tm.committedTxns.add(txn.id) + tm.committedTxnsSet.incl(txn.id) tm.activeTxns.del(txn.id) tm.deadlockDetector.removeTxn(uint64(txn.id)) release(tm.lock) @@ -258,6 +268,7 @@ proc abortTxn*(tm: TxnManager, txn: Transaction): bool = return false txn.state = tsAborted tm.activeTxns.del(txn.id) + tm.abortedTxns.incl(txn.id) tm.deadlockDetector.removeTxn(uint64(txn.id)) release(tm.lock) return true diff --git a/src/barabadb/core/raft.nim b/src/barabadb/core/raft.nim index 6ec8379..0e84589 100644 --- a/src/barabadb/core/raft.nim +++ b/src/barabadb/core/raft.nim @@ -290,7 +290,7 @@ proc handleAppendReply*(node: RaftNode, peerId: string, reply: RaftMessage) = matchIndices.add(idx) matchIndices.sort() - let medianIdx = matchIndices[matchIndices.len div 2] + let medianIdx = matchIndices[(matchIndices.len - 1) div 2] if medianIdx > node.commitIndex: if medianIdx <= node.lastLogIndex and node.log[medianIdx - 1].term == node.currentTerm: diff --git a/src/barabadb/protocol/auth.nim b/src/barabadb/protocol/auth.nim index db073a5..a56282b 100644 --- a/src/barabadb/protocol/auth.nim +++ b/src/barabadb/protocol/auth.nim @@ -2,6 +2,7 @@ import std/strutils import std/base64 import std/tables +import checksums/sha2 type AuthMethod* = enum @@ -51,12 +52,33 @@ proc base64UrlDecode(data: string): string = s &= "=" return decode(s) -proc simpleHash(data: string, key: string): string = - var prefix = data & key - var h: uint64 = 5381 - for ch in prefix: - h = ((h shl 5) + h) + uint64(ord(ch)) - return $h +proc hmacSha256(key, message: string): string = + var k = key + if k.len > 64: + var ctx = initSha_256() + ctx.update(k.toOpenArray(0, k.len-1)) + let hash = ctx.digest() + k = $hash + while k.len < 64: + k &= "\x00" + + var ipad = newString(64) + var opad = newString(64) + for i in 0..<64: + ipad[i] = chr(ord(k[i]) xor 0x36) + opad[i] = chr(ord(k[i]) xor 0x5c) + + var innerCtx = initSha_256() + innerCtx.update(ipad.toOpenArray(0, ipad.len-1)) + innerCtx.update(message.toOpenArray(0, message.len-1)) + let innerHash = innerCtx.digest() + + var outerCtx = initSha_256() + outerCtx.update(opad.toOpenArray(0, opad.len-1)) + outerCtx.update(innerHash.toOpenArray(0, innerHash.len-1)) + let outerHash = outerCtx.digest() + + return $outerHash proc createToken*(am: AuthManager, claims: JWTClaims): string = let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}") @@ -64,7 +86,7 @@ proc createToken*(am: AuthManager, claims: JWTClaims): string = "{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role & "\",\"database\":\"" & claims.database & "\"}") let data = header & "." & payload - let signature = simpleHash(data, am.secretKey) + let signature = hmacSha256(am.secretKey, data) am.tokens.add(data & "." & base64UrlEncode(signature)) return am.tokens[^1] @@ -73,7 +95,7 @@ proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) = if parts.len != 3: return (false, JWTClaims()) let data = parts[0] & "." & parts[1] - let sig = simpleHash(data, am.secretKey) + let sig = hmacSha256(am.secretKey, data) if base64UrlEncode(sig) != parts[2]: return (false, JWTClaims()) # Parse payload @@ -132,8 +154,8 @@ proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult = if creds.username in am.users: let stored = am.users[creds.username] # SCRAM-SHA-256: client sends SHA-256(password) as payload - let clientHash = if creds.payload.len > 0: creds.payload else: simpleHash("", am.secretKey) - if stored == clientHash or stored == simpleHash(creds.payload, am.secretKey): + let clientHash = if creds.payload.len > 0: creds.payload else: hmacSha256(am.secretKey, "") + if stored == clientHash or stored == hmacSha256(am.secretKey, creds.payload): return AuthResult(authenticated: true, username: creds.username, role: "user", database: "default") return AuthResult(authenticated: false, error: "Invalid SCRAM credentials") diff --git a/src/barabadb/query/executor.nim b/src/barabadb/query/executor.nim index a8fb156..e2f7aed 100644 --- a/src/barabadb/query/executor.nim +++ b/src/barabadb/query/executor.nim @@ -410,6 +410,8 @@ proc parseRowData(valStr: string): Table[string, string] = let v = part[eqPos+1..^1].strip() result[k] = v +proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] + proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = nil): string = if expr == nil: return "" case expr.kind @@ -593,7 +595,11 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = let v = evalExpr(expr.unExpr, row) return if not isNull(v): "true" else: "false" else: return "false" - of irekExists: return "false" + of irekExists: + if ctx != nil: + let rows = executePlan(ctx, expr.existsSubquery) + return if rows.len > 0: "true" else: "false" + return "false" else: return "" proc lowerExpr*(node: Node): IRExpr diff --git a/src/barabadb/storage/lsm.nim b/src/barabadb/storage/lsm.nim index 758910a..d713ef6 100644 --- a/src/barabadb/storage/lsm.nim +++ b/src/barabadb/storage/lsm.nim @@ -31,6 +31,7 @@ type maxSize: int SSTable* = object + id*: int path*: string index*: Table[string, int64] bloom*: BloomFilter @@ -186,6 +187,7 @@ proc writeSSTable*(entries: seq[Entry], path: string, level: int): SSTable = if maxK == "" or entry.key > maxK: maxK = entry.key result = SSTable( + id: -1, path: path, index: idxTable, bloom: bloom, @@ -238,6 +240,7 @@ proc loadSSTable*(path: string): SSTable = bloom.deserialize(bloomData) result = SSTable( + id: -1, path: path, index: idxTable, bloom: bloom, @@ -294,6 +297,8 @@ proc close*(sst: var SSTable) = # LSMTree API # ---------------------------------------------------------------------- +proc flushUnsafe(db: LSMTree) {.gcsafe.} + proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree = createDir(dir) createDir(dir / "sstables") @@ -306,13 +311,14 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree = if kind == pcFile and path.endsWith(".sst"): try: var sst = loadSSTable(path) - sstables.add(sst) let name = splitFile(path).name - nextId = max(nextId, parseInt(name) + 1) + sst.id = parseInt(name) + sstables.add(sst) + nextId = max(nextId, sst.id + 1) except: discard # skip corrupt SSTables - sstables.sort(proc(a, b: SSTable): int = cmp(a.minKey, b.minKey)) + sstables.sort(proc(a, b: SSTable): int = cmp(b.id, a.id)) new(result) initLock(result.lock) @@ -328,8 +334,9 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree = # WAL crash recovery — replay unflushed entries into memTable let walPath = dir / "wal" / "wal.log" if fileExists(walPath): + var stream: FileStream = nil try: - let stream = newFileStream(walPath, fmRead) + stream = newFileStream(walPath, fmRead) if stream != nil: var magic: uint32 = 0 var version: uint32 = 0 @@ -352,16 +359,20 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree = if stream.readData(addr value[0], valLen.int) != valLen.int: break case WalEntryKind(kind) of wekPut: - discard result.memTable.put(key, value, timestamp) + if not result.memTable.put(key, value, timestamp): + result.flushUnsafe() + discard result.memTable.put(key, value, timestamp) of wekDelete: - discard result.memTable.put(key, @[], timestamp) # tombstone + if not result.memTable.put(key, @[], timestamp, deleted = true): + result.flushUnsafe() + discard result.memTable.put(key, @[], timestamp, deleted = true) of wekCommit: discard of wekCheckpoint: discard + finally: + if stream != nil: stream.close() - except: - discard proc put*(db: LSMTree, key: string, value: seq[byte]) = acquire(db.lock) @@ -369,6 +380,8 @@ proc put*(db: LSMTree, key: string, value: seq[byte]) = let ts = uint64(getMonoTime().ticks()) db.wal.writePut(cast[seq[byte]](key), value, ts) if not db.memTable.put(key, value, ts): + if db.immutableMem.len > 0: + db.flushUnsafe() db.immutableMem = db.memTable db.memTable = newMemTable(db.memMaxSize) discard db.memTable.put(key, value, ts) @@ -378,7 +391,12 @@ proc delete*(db: LSMTree, key: string) = defer: release(db.lock) let ts = uint64(getMonoTime().ticks()) db.wal.writeDelete(cast[seq[byte]](key), ts) - discard db.memTable.put(key, @[], ts, deleted = true) + if not db.memTable.put(key, @[], ts, deleted = true): + if db.immutableMem.len > 0: + db.flushUnsafe() + db.immutableMem = db.memTable + db.memTable = newMemTable(db.memMaxSize) + discard db.memTable.put(key, @[], ts, deleted = true) proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) = let (found, entry) = db.memTable.get(key) @@ -438,8 +456,9 @@ proc flushUnsafe(db: LSMTree) = inc db.nextSSTableId var sst = writeSSTable(toFlush.entries, path, level = 0) + sst.id = db.nextSSTableId - 1 db.sstables.add(sst) - db.sstables.sort(proc(a, b: SSTable): int = cmp(a.minKey, b.minKey)) + # SSTables are kept in insertion order (newest last) so getUnsafe can search newest-first db.wal.writeCommit(uint64(getMonoTime().ticks())) db.wal.sync() diff --git a/src/barabadb/storage/recovery.nim b/src/barabadb/storage/recovery.nim index 195b502..3353fd0 100644 --- a/src/barabadb/storage/recovery.nim +++ b/src/barabadb/storage/recovery.nim @@ -168,7 +168,7 @@ proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult = proc totalEntries*(rec: CrashRecovery): int = rec.entries.len proc summary*(rec: CrashRecovery): string = - let r = rec.recover() + let r = rec.analyze() result = "WAL Recovery Summary:\n" result &= " Total entries: " & $r.totalEntries & "\n" result &= " Redone (committed): " & $r.redone & "\n" diff --git a/src/barabadb/storage/wal.nim b/src/barabadb/storage/wal.nim index e22558d..05ce2d9 100644 --- a/src/barabadb/storage/wal.nim +++ b/src/barabadb/storage/wal.nim @@ -1,6 +1,7 @@ ## WAL — Write-Ahead Log for durability import std/os import std/streams +import std/posix const WALMagic* = 0x42415241'u32 # "BARA" @@ -77,9 +78,17 @@ proc writeCommit*(wal: var WriteAheadLog, timestamp: uint64) = proc sync*(wal: var WriteAheadLog) = wal.stream.flush() + let fd = posix.open(wal.path, O_RDONLY) + if fd != -1: + discard posix.fsync(fd) + discard posix.close(fd) proc close*(wal: var WriteAheadLog) = wal.stream.flush() + let fd = posix.open(wal.path, O_RDONLY) + if fd != -1: + discard posix.fsync(fd) + discard posix.close(fd) wal.stream.close() proc entryCount*(wal: WriteAheadLog): uint64 = wal.entryCount