What survives an interrupted write?
StrataKV is an embedded, LSM-style key-value store written in Go with no
external module dependencies. It runs inside the program that uses it; there
is no server. The current release is v2.0.0
(github.com/Adityarya11/StrataKV/v2). v2 is about making partial writes and
corruption visible, instead of treating every failed read as a missing key.
Make the recovery boundary explicit
Writes go to a write-ahead log before they change the memtable. Each record has a CRC-32C checksum. An atomic write batch is written as one group, with every record except the last marked pending, so recovery applies the whole batch or none of it. On reopen, replay keeps the valid prefix of the log and cuts off an unreadable tail. It does not pretend to recover corrupted data.
An integration test cuts the last record of a batch, reopens the database, and checks both sides: the write before the batch survives, and none of the 50 batched writes come back. Read the interrupted-batch test.
Three bugs v2 fixed
- A torn write at the end of the WAL made the database refuse to open.
- Compaction lost 128 of 600 keys and still returned no error.
- The Bloom filter's false-positive rate was 86% at 10k keys per segment. With proper sizing it is now about 1%.
Trade read work for simpler storage
Flushed data goes into immutable segment files. Segments are not sorted and have no index, so a lookup that reaches a segment scans it. Bloom filters skip segments that cannot contain the key. Compaction merges segments, stops if it cannot read a source, and verifies the new segment before deleting the old ones.
Compaction only runs when the caller invokes it, and it holds an exclusive
lock while it runs. Stats() exposes segment and memtable state so the
application can decide when to compact.
What the benchmarks measured
Run on 2026-10-06 on my laptop (Ryzen 5 6600H, NVMe SSD, Windows 11) against real files with real fsync. Keys and values are synthetic.
- A durable write is an fsync. A
SyncAlwaysput costs about 2.4 ms whatever the value size. - Batching spreads that cost: 346 records/s with batches of 1, 45,867
records/s with batches of 1,000 (1 KB values,
SyncAlways). - Startup: v2 reads one trailer per segment instead of rescanning every byte. Opening 25 segments of 64 B values went from 825 ms in v1.0.3 to 2.9 ms.
- Bloom filters make lookups for missing keys 96 to 132 times faster with 64 B and 1 KB values, and 23 to 27 times faster with 8 KB values.
- On one churn workload (10k keys, 30k overwrites, 2.5k deletes), compaction reclaimed 76.2% of the bytes on disk. That figure belongs to that workload.
Two results went the other way:
- Compaction makes point reads slower. With 10 segments of 1 KB values, a hit took 585 µs before compaction and 5.6 ms after (9.6 times slower). One big segment with no index means scanning much more data.
- v2 is slower than v1.0.3 in 5 of 30 read configurations, all with 8 KB values. v2 reads and checksums every value in a segment it scans.
Integration and current limits
Blan uses StrataKV to cache interpreter output. Its backend still pins a pre-v2 revision and never calls compaction, so it does not use the v2 API described here.
Not built yet: automatic compaction, an index inside segments, range scans, TTL or eviction, MVCC, replication, and general transactions. Atomic write batches are a narrower guarantee than transactions. It is a single-node library.