projects / 02
basalt
persistent in-memory KV store
A persistent in-memory key-value store in Go. State lives in a 32-way sharded map (fnv32, one RWMutex per shard) and every mutation is appended to an AOF before the map changes. On startup the log is read through mmap and replayed; torn writes at the tail are truncated and CRC mismatches are reported as corruption.
- concurrent get
- ~60ns
- concurrent get
- writes / sec
- ~478k
- writes / sec
- allocs on writes
- 0 B/op
- allocs on writes
how it works
- 01
Write
Set and Delete hash the key with fnv32 to one of 32 shards, take that shard's lock, append a record to the AOF and only then update the map. If the append fails, the map is left unchanged.
- 02
Encode
Each record is CRC32, op, varint key length, varint value length, key and value. Buffers come from a sync.Pool and the varints live on the stack, so the write path allocates nothing.
- 03
Read
Get takes a read lock on one shard only, so reads on different shards never contend.
- 04
Recover
On startup the AOF is memory-mapped and replayed. A record cut short at the end of the file is treated as a torn write and truncated; a CRC mismatch on a complete record is reported as corruption.
- 05
Compact
Every five minutes the store is snapshotted into a new log while writers keep going, with their changes buffered on the side. Writes pause only for the final step: replay the buffer, fsync, and rename the new log over the old one.
design decisions
Append-only log over snapshots
Appending is sequential and cheap, and replaying the log rebuilds the exact state. Compaction keeps the file from growing forever.
32 shards instead of one lock
A single RWMutex serialises every writer. Sharding by key hash lets unrelated keys proceed in parallel; writes to the same shard are still ordered, so log order matches map order.
mmap for recovery
Recovery walks the whole file once; mapping it avoids issuing a read call per record.
highlights
- Each record is CRC32 · op · varint lengths · key · value, so bit-rot is detected instead of replayed
- Zero-allocation write path: stack varints and sync.Pool buffers keep the GC out of it
- Compaction snapshots while writers keep going, then pauses writes only for the final log swap
known limitations
- The AOF is fsynced once per second, so a power loss can drop up to about a second of acknowledged writes.
- A corrupted length field mid-file looks like a torn write, so everything after it is truncated.
- The whole dataset must fit in RAM, and compaction briefly needs disk for a second copy.
- Local CLI only, with no network server yet; runs on Linux and macOS (use WSL on Windows).
stack
- go
- aof
- mmap
- crc32
- sync.Pool