Files
Baradb/src/barabadb/storage/compaction.nim
T
dimgigov 42043f3946
CI / test (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
v1.1.7: deep security & reliability audit — 33 bugs fixed
Critical (5):
- Reject empty JWT secret when authEnabled (server.nim)
- Fix 2PC marking uncontacted participants as prepared/committed (disttxn.nim)
- Fix Raft commit index calculation for even-sized clusters (raft.nim)
- Fix REP/DISTTXN protocol auth bypass (server.nim)
- Fix HTTP backup/restore path traversal (httpserver.nim)

High (11):
- Fix WAL write race with flush (lsm.nim)
- Fix MVCC savepoint/rollback deep-copy writeSet (mvcc.nim)
- Fix table mutation during deadlock iteration (mvcc.nim)
- Fix LIMIT 0 returning all rows (executor.nim)
- Fix COUNT(col) counting NULL values — 3 locations (executor.nim)
- Fix EXISTS subquery lowering missing subqueryPlan (executor.nim)
- Fix Raft appendEntries/applyCommitted array vs logical index (raft.nim)
- Fix timing attacks on constantTimeCompare and SCRAM (auth.nim, scram.nim)
- Fix B-tree leaf merge phantom separator key (btree.nim)
- Fix SSL verifyPeer not applied to newContext (ssl.nim)
- Fix sharding connectWithTimeout missing SO_ERROR check (sharding.nim)
- Fix sync replication returning success on partial ack (replication.nim)
- Fix WebSocket JWT expiration not validated (websocket.nim)

Medium (13):
- Fix writeSSTable partial file → tmp + atomic rename (lsm.nim)
- Fix multi-CTE table loss (executor.nim)
- Fix nl_to_sql DML restricted to superuser (executor.nim)
- Fix unbounded plan cache — max 10000 (adaptive.nim)
- Fix migration lock crash persistence — timestamp + stale detection (executor.nim)
- Fix admin panel auth (httpserver.nim)
- Fix MVCC unbounded txn tracking — prune in compactVersions (mvcc.nim)
- Fix connection pool maxLifetime check (pool.nim)
- Fix JWT JSON parser backslash escapes (auth.nim)
- Fix substr(s, start) returning single char (udf.nim)
- Fix loadSSTable minimum file-size check (lsm.nim)
- Fix compaction mmap leak (compaction.nim)
- Fix JSON injection in hybrid_search_filtered (executor.nim)

Low (4):
- Raft loadState logs error instead of silent discard
- Replication healthCheck double-close fixed
- Lexer readIdent double column counting fixed
- WebSocket frame 32-bit overflow guard

All 448 tests passing, 0 failures. Bump version to 1.1.7.
2026-05-29 14:17:41 +03:00

258 lines
6.8 KiB
Nim

## SSTable Compaction — size-tiered and leveled strategies
import std/tables
import std/algorithm
import std/os
import std/math
import ../storage/lsm
const
MaxLevel* = 7
LevelMultiplier* = 10 # each level is 10x the previous
type
SSTableMeta* = object
path*: string
level*: int
minKey*: string
maxKey*: string
entryCount*: int
sizeBytes*: int
createdAt*: int64
CompactionResult* = object
inputTables*: seq[SSTableMeta]
outputTables*: seq[SSTableMeta]
entriesRead*: int
entriesWritten*: int
CompactionStrategy* = ref object
levels*: seq[seq[SSTableMeta]]
dataDir*: string
maxSizePerLevel*: seq[int]
proc newCompactionStrategy*(dataDir: string): CompactionStrategy =
result = CompactionStrategy(
levels: newSeq[seq[SSTableMeta]](MaxLevel),
dataDir: dataDir,
maxSizePerLevel: newSeq[int](MaxLevel),
)
for i in 0..<MaxLevel:
result.levels[i] = @[]
result.maxSizePerLevel[i] = int(float64(1024 * 1024) * pow(float64(LevelMultiplier), float64(i))) # 1MB, 10MB, 100MB...
proc addTable*(cs: CompactionStrategy, meta: SSTableMeta) =
if meta.level < MaxLevel:
cs.levels[meta.level].add(meta)
proc totalSize*(cs: CompactionStrategy, level: int): int =
result = 0
for t in cs.levels[level]:
result += t.sizeBytes
proc needsCompaction*(cs: CompactionStrategy, level: int): bool =
if level >= MaxLevel - 1:
return false
return cs.totalSize(level) > cs.maxSizePerLevel[level]
proc pickTablesForCompaction*(cs: CompactionStrategy, level: int): seq[SSTableMeta] =
if cs.levels[level].len == 0:
return @[]
# Sort by creation time, pick oldest
var sorted = cs.levels[level]
sorted.sort(proc(a, b: SSTableMeta): int = cmp(a.createdAt, b.createdAt))
let count = min(sorted.len, 4) # compact up to 4 tables at once
return sorted[0..<count]
proc compact*(cs: CompactionStrategy, level: int): CompactionResult =
let tables = cs.pickTablesForCompaction(level)
if tables.len == 0:
return CompactionResult()
var entriesRead = 0
var allEntries: seq[Entry] = @[]
# Read all entries from SSTable files
var failedLoad = false
for t in tables:
try:
var sst = loadSSTable(t.path)
for key, offset in sst.index:
let (found, entry) = readSSTableEntry(sst, key)
if found:
allEntries.add(entry)
inc entriesRead
sst.close()
except CatchableError as e:
echo "[ERROR] Failed to load SSTable for compaction: ", t.path, ": ", e.msg
failedLoad = true
break
if failedLoad:
return CompactionResult()
# Sort by key, then by timestamp (newest first for dedup)
allEntries.sort(proc(a, b: Entry): int =
let kc = cmp(a.key, b.key)
if kc != 0: return kc
return cmp(b.timestamp, a.timestamp) # newest first
)
# Deduplicate: keep only the newest version of each key
var merged: seq[Entry] = @[]
var lastKey = ""
for entry in allEntries:
if entry.key != lastKey:
merged.add(entry)
lastKey = entry.key
# Keep tombstones to prevent deleted keys from resurrecting in lower levels
var final: seq[Entry] = @[]
for entry in merged:
final.add(entry)
# Write merged SSTable
let outputPath = cs.dataDir / "sstables" / ("level_" & $level & "_" & $tables[0].createdAt & ".sst")
var sst = writeSSTable(final, outputPath, level + 1)
# Use actual file size instead of rough guess
let actualSize = try: getFileSize(outputPath) except: final.len * 64
let outputMeta = SSTableMeta(
path: outputPath,
level: level + 1,
minKey: sst.minKey,
maxKey: sst.maxKey,
entryCount: final.len,
sizeBytes: actualSize,
createdAt: tables[^1].createdAt,
)
# Verify output SSTable before deleting sources
let (ok, msg) = verifySSTable(outputPath)
if not ok:
echo "[ERROR] Compaction output verification failed: ", msg
try: removeFile(outputPath) except: discard
return CompactionResult()
# Remove old SSTable files
for t in tables:
try:
removeFile(t.path)
except CatchableError as e:
echo "[WARN] Failed to remove old SSTable: ", t.path, ": ", e.msg
# Update level arrays
var newTables: seq[SSTableMeta] = @[]
for t in cs.levels[level]:
var found = false
for picked in tables:
if t.path == picked.path:
found = true
break
if not found:
newTables.add(t)
cs.levels[level] = newTables
if level + 1 < MaxLevel:
cs.levels[level + 1].add(outputMeta)
return CompactionResult(
inputTables: tables,
outputTables: @[outputMeta],
entriesRead: entriesRead,
entriesWritten: final.len,
)
proc levelCount*(cs: CompactionStrategy): int =
result = 0
for level in cs.levels:
if level.len > 0:
inc result
proc tableCount*(cs: CompactionStrategy): int =
result = 0
for level in cs.levels:
result += level.len
# Page Cache — LRU cache for SSTable pages
type
CacheEntry* = ref object
key*: string
data*: seq[byte]
accessCount*: int
dirty*: bool
PageCache* = ref object
capacity: int
pages: Table[string, CacheEntry]
accessOrder: seq[string]
hits*: int
misses*: int
proc newPageCache*(capacity: int = 1000): PageCache =
PageCache(
capacity: capacity,
pages: initTable[string, CacheEntry](),
accessOrder: @[],
hits: 0,
misses: 0,
)
proc evict*(cache: PageCache) =
if cache.pages.len >= cache.capacity:
# Remove least recently used
if cache.accessOrder.len > 0:
let oldest = cache.accessOrder[0]
cache.accessOrder.delete(0)
cache.pages.del(oldest)
proc put*(cache: PageCache, key: string, data: seq[byte]) =
if key in cache.pages:
cache.pages[key].data = data
# Move to end of access order
var newOrder: seq[string] = @[]
for k in cache.accessOrder:
if k != key:
newOrder.add(k)
newOrder.add(key)
cache.accessOrder = newOrder
else:
cache.evict()
cache.pages[key] = CacheEntry(
key: key, data: data,
accessCount: 1, dirty: false,
)
cache.accessOrder.add(key)
proc get*(cache: PageCache, key: string): (bool, seq[byte]) =
if key in cache.pages:
inc cache.hits
cache.pages[key].accessCount += 1
# Move to end
var newOrder: seq[string] = @[]
for k in cache.accessOrder:
if k != key:
newOrder.add(k)
newOrder.add(key)
cache.accessOrder = newOrder
return (true, cache.pages[key].data)
inc cache.misses
return (false, @[])
proc contains*(cache: PageCache, key: string): bool =
return key in cache.pages
proc hitRate*(cache: PageCache): float64 =
let total = cache.hits + cache.misses
if total == 0: return 0.0
return float64(cache.hits) / float64(total)
proc len*(cache: PageCache): int = cache.pages.len
proc capacity*(cache: PageCache): int = cache.capacity
proc clear*(cache: PageCache) =
cache.pages.clear()
cache.accessOrder.setLen(0)
cache.hits = 0
cache.misses = 0