ReazGan / tarndb
git clone https://github.com/ReazGan/tarndb.git
Crash-safe embedded key-value store in C
—
stars, 24h
+50
stars, 7 days · #896
+50
stars, 30 days · #5,902
50
★ total · +50% this week
21
forks · 0 open issues
Star history starts 2026-10-20; the chart appears after the second daily snapshot.
LanguageC
LicenseMIT
Created2026-10-15 (5d ago)
Last push5d ago
Watchers0
OwnerUser
GitHub topics
Links
- GitHub: github.com/ReazGan/tarndb
- Releases · Issues · Hacker News mentions
README excerpt
# tarn A small embedded key-value store in C11 that does not lose writes. Every put is appended to a segment file and fsynced before the call returns. After a crash, a `kill -9` or a power cut, tarn comes back with every write it acknowledged and never hands back a half-written one. No dependencies; builds on Linux, macOS and Windows. [code] The whole API is in include/tarn.h. ## Build [code] This gives you `libtarn`, the `tarn` command line tool and `tarn_bench`. To use it from another CMake project, `add_subdirectory(tarn)` and link against `tarn`. ## How it works **Segments.** Data lives in append-only files named `00000001.seg`, `00000002.seg` and so on. Records are only ever added at the end of the newest one; when it passes 64 MiB a new one is started. Nothing is ever overwritten in place. **Records.** Each record has a 16 byte header: a CRC32C of the header itself, the key length, the value length and a CRC32C of the key and value. A delete is a record with no value. The header checksum is separate on purpose: if a value gets damaged, the record boundary is still trustworthy and only that one record is lost instead of everything after it. **Index.** An in-memory hash table maps every key to the segment and offset of its newest record, so a get is one read. The table uses linear probing with backward-shift deletion (no tombstones, lookups stay short as keys come and go) and keyed SipHash-2-4, so keys picked by an attacker cannot be made to collide. **Recovery.** On open every segment is read front to back and every record is checked. What the checker finds decides what happens: - A torn record at the very end of the newest segment is what a crash in the middle of a write looks like. It is cut off and appends continue from there. Its unfinished bytes are never searched for records, so a value that happens to contain a copy of a tarn record cannot be mistaken for one. - A record with a sound header but a bad value is skipped. The header still says where the next record starts. - A damaged header hides where the next record starts. tarn slides forward a byte at a time until a header and the record behind it both check out, so one bad sector does not take the rest of the file with it. - A zero-filled tail, which some file systems leave after a power cut, is treated as torn. Skipped records are counted in `tarn_stats.corrupt_records`, bytes cut off in `recovered_bytes`. Values are checked again on every `tarn_get`, so damage that happens while the store is open is reported as `TARN_CORRUPT`, never returned as data. **Compaction.** `tarn_compact` copies every live record out of the old segments into one new file, then renames it over the newest of them. The rename is the commit point. The new file's header records the range of segments it replaces, so if the process dies after the rename but before the old files are deleted, the next open finds them and finishes the job. A deleted key never comes back from a half-finished compaction;