solistore
An embedded, ordered key-value store built as an LSM tree, in pure Rust. It fsyncs every commit by default and makes concurrent commits share that fsync. It is written to replace RocksDB under SoliDB.
commits ──► group commit ──► one WAL write + one fsync per group
│
▼
memtable (skiplist, entries in per-thread arenas) ──seal──► immutable memtables
│ flush
▼
L0 ▢ ▢ ▢ ▢ overlapping files, newest first
L1 ▢▢▢▢▢▢▢▢ 512 MiB of sorted, non-overlapping files ─┐
L2 ▢▢▢▢▢▢▢▢▢… ×10 per level ├─ leveled compaction,
… ┘ parallel subcompactions
Status — not used in production yet. solistore passed its go/no-go gates against RocksDB 11.8.1 on 2026-10-14. SoliDB now runs on it behind a feature flag, where it is ahead of RocksDB on every phase measured (see Benchmarks). The on-disk format may still change before 1.0. See the integration plan.
Why another storage engine
SoliDB used RocksDB, which has three costs:
- Build time. Its C++ was most of SoliDB's build: CI measured 95 minutes of compilation for 18 seconds of tests. It also needs a cross g++ toolchain for musl and aarch64, and Windows was limited to a compile-only check.
- Memory. It is hard to account for and harder to bound, with per-column-family structures that grow with the number of collections.
- Durability. SoliDB's ordinary writes were not fsynced, because an fsync per write was too slow.
solistore answers each of these with a design choice:
- Pure Rust, no
-syscrate. Nothing in the dependency graph needs a C compiler, and CI enforces it (scripts/check_no_c_toolchain.sh). - One memory budget. A shared block cache holds data blocks. Index and filter memory is charged to the same cache.
- Durable commits that stay fast. Writers queue behind a leader, which writes the whole group to the WAL and fsyncs once for all of them.
- Cheap column families. All of them share one LSM. Creating or dropping one is a single MANIFEST record, and opening a database with thousands of them reads no table file.
Features
- Column families created and dropped at run time.
- Atomic write batches across column families: readers see a batch entirely or not at all.
getreturnsBytes, so a block-cache hit is served without a copy.multi_getis also available.- Bounded iterators in both directions, with RocksDB's
Start,EndandFrom(key, direction)modes;From(k, Reverse)is seek-for-prev. - Range deletes:
- Kept at the version level rather than in tables.
- A file wholly covered by a delete is dropped without being read.
- Files that straddle the deleted range are compacted first.
- Leveled compaction with parallel subcompactions and trivial moves. Writers are slowed and then stalled on level-0 backlog.
- Point-in-time checkpoints made of hard links.
- Metrics:
- Memtable bytes, table-reader bytes, block-cache usage and files per level.
- Approximate size of a key range.
- Group-commit sizes, stall times and compaction write volume.
- Two durability modes:
| Mode | A commit returns when… | Process crash | Power loss |
|---|---|---|---|
Durability::Full (default) |
its group's fsync completed | loses nothing | loses nothing acknowledged |
Durability::Eventual { sync_interval } |
it reached the OS; a background thread fsyncs every interval | loses nothing | loses at most sync_interval of acknowledged writes, never a write without every later one |
In either mode, a write with WriteOptions { sync: true } waits for its own
fsync. An fsync failure poisons the database, which then refuses further
writes rather than retrying (see "fsyncgate").
Quick start
use Duration;
use ;
let db = open?;
let users = db.create_cf?;
let index = db.create_cf?;
// Atomic across column families.
let mut batch = new;
batch.put;
batch.put;
db.write?;
let id = db.get?; // Option<Bytes>
for item in db.iterator?
let mut batch = new;
batch.delete_range; // [start, end)
db.write?;
db.checkpoint?;
The full program is
crates/solistore/examples/quickstart.rs.
Run it with cargo run -p solistore --example quickstart.
How it is tested
A storage engine is only as good as its crash behaviour, so most of the test code exercises failure paths:
- Model-based property tests. These run random sequences against a
BTreeMapmodel: puts, deletes, range deletes, batches, gets, bounded scans in both directions, flushes, compactions, reopens, column-family create and drop, and opening a checkpoint. Tiny memtables and files force every background path to run. SetPROPTEST_CASESto go deeper; 20,000 cases found a WAL-recycling race that 256 did not. - A deterministic crash sweep. A fault-injecting filesystem (
FaultFs) crashes the database at every I/O operation of a workload and leaves torn, unsynced tails. After each crash, the reopened database must equal the model at the last acknowledged commit. - Fault injection.
FaultFsalso makes fsyncs fail or run slow, to exercise poisoning and group commit. - Process kills. The benchmark's
crashtest SIGKILLs a writer 100 times mid-load and checks every acknowledged batch. Itsvisibilitytest checks that no reader ever sees part of a batch. - Mutation testing. Bugs are injected on purpose to check that the tests above catch them.
Benchmarks
All figures are against RocksDB 11.8.1 with SoliDB's production profile.
Ratios are solistore throughput over RocksDB's; above 1.0 is faster.
solistore runs in Eventual mode (100 ms), and RocksDB with SoliDB's
sync=false writes, unless stated otherwise.
Inside SoliDB, whole server
The same SoliDB binary ran on each engine, with 1M documents and two indexes. The load came over SoliDB's native driver with the query cache off; the server had 8 cores, the client 8 others. Figures are medians of 3 runs.
| Phase | 64 connections | 256 connections |
|---|---|---|
| insert, one document per request | 1.11× | 1.07× |
| get by key | 1.04× | 1.08× |
| update (read, merge, write) | 1.07× | 1.14× |
| query on an index (equality) | 1.05× | 1.10× |
| query on an index (range) | 1.05× | 1.10× |
| mixed, 90% reads | 0.99× | 1.00× |
SDBQL queries, 64 connections (8 for the scan and aggregate):
| Query | Ratio |
|---|---|
FILTER d._key == @k |
1.02× |
index equality, LIMIT 10 |
1.08× |
COLLECT WITH COUNT over ~1,000 documents |
1.06× |
| projection of 1,000 rows | 1.04× |
SORT on an index, LIMIT 10 |
1.05× |
| join (correlated subquery) | 1.08× |
| 100 raw rows over the driver | 1.04× |
UPDATE |
1.04× |
INSERT |
1.16× |
| filter on an unindexed field (scan) | 1.12× |
COLLECT over 100,000 documents |
1.12× |
solistore also uses less CPU per operation, for example 27 µs per update
against 32 µs. Peak RSS is 1.02–1.08× RocksDB's at 1M documents; at 5M,
with 64 connections each reading 5,000 documents per query, it is 0.66×
(1,355 MB against 2,042 MB), because point reads hand out a copy of a small
value rather than a slice that keeps its 4 KiB block alive
(bench/results/2026-10-15-point-read-copies). With 256 connections,
RocksDB's p99 update latency is 4.4 ms and solistore's 3.0 ms.
Storage engines alone
bench/ runs a SoliDB-shaped workload on each engine directly: a write is
the document plus three index entries, and the phases are gets, scans,
updates, fsynced transactions and a ~1,232-collection catalog.
At 5M documents, medians of 3, the go/no-go series of 2026-10-14
(bench/results/2026-10-14-m7):
| Phase | Eventual vs RocksDB |
|---|---|
| bulk load | 0.81× |
| updates | 0.82× |
| mixed read/write | 1.09× |
| get, uniform / hot / missing | 1.03× / 1.23× / 1.65× |
| scans | 1.34–1.43× |
| fsynced commits, 1 / 8 threads | 1.21× / 1.24× |
In full durability mode, solistore fsyncs every commit. It still runs 2.9× (1 thread) and 1.4× (8 threads) RocksDB's fsynced-commit throughput.
The gates that block integration all passed:
- 100 SIGKILL crashes with 14.87M acknowledged writes: 0 lost, 0 torn batches.
- 0 visibility violations in 42M checks.
- Reopen under 0.03 s.
- 260 MB reclaimed after a drop.
- RSS 0.95× RocksDB's, disk 1.09×.
The engine-only bench predates the commit-pipeline and memtable-sizing work that put solistore ahead inside SoliDB; it has not been rerun at 5M since.
Against SurrealKV
SurrealKV 1.0.0 is in the same bench, with the budget the other engines
get: 512 MiB block cache, 64 MiB memtable, 4 KiB blocks and Snappy. At 1M
documents (single runs, bench/results/2026-10-14-surrealkv):
| Phase | RocksDB | solistore | SurrealKV |
|---|---|---|---|
| load | 434k | 398k | 335k |
| fsynced commits, 8 threads | 63.5k | 214k | 68k |
| get, uniform | 3.64M | 3.23M | 2.57M |
| scan, 100 keys | 59k | 82k | 40k |
| update | 651k | 484k | 376k |
| mixed | 290k | 346k | 180k |
| peak RSS | 948 MB | 973 MB | 1,176 MB |
With every commit fsynced, solistore writes about twice as fast as SurrealKV (load 170k against 86k, updates 174k against 90k).
With its factory settings (32 MiB cache, no compression), SurrealKV's uniform gets fall to 307k a second. Its published benchmark used 500,000 keys, which fit in its memtable and cache.
Raw results are in bench/results/, as JSON lines per phase.
Building
The benchmark is a separate workspace, so RocksDB's C++ never enters the engine's build:
MSRV is Rust 1.91, SoliDB's floor. CI checks Linux, macOS, Windows and musl.
Repository layout
crates/solistore/src/
db.rs open, commit pipeline, reads, column families, checkpoints
batch.rs WriteBatch and its WAL encoding
log.rs record framing shared by the WAL and the MANIFEST
memtable.rs skiplist memtable, entries in per-thread arenas
sst/ table format: blocks, bloom filters, block cache, reader/builder
version.rs versions, super-versions (arc-swap), level iterators
manifest.rs version edits and recovery
flush.rs memtable → level 0
compaction.rs pickers, subcompactions, trivial moves
iter.rs merging iterator and the user-facing iterator
fs.rs filesystem trait: StdFs, and FaultFs for crash tests
bench/ SoliDB-shaped benchmark against RocksDB
docs/ integration plan for SoliDB
Roadmap
- M0–M7: WAL and group commit, tables, compaction, range tombstones, block cache, checkpoints and metrics, then the go/no-go benchmark.
- SoliDB on solistore behind a feature, with the engine recorded per data directory. Ahead of RocksDB on every measured phase.
- A migration tool from RocksDB data directories (in progress).
- A trial on real workloads, then removing RocksDB from SoliDB.
- Dynamic level sizing, a fully arena-allocated skiplist,
loomon the commit path, Miri on theunsafe, decoder fuzzing, and crash tests on NTFS.
Licence
O'Saasy License © 2024-2026 SoliSoft SARL, the same as SoliDB. It is MIT-style, except that you may not offer the software itself as a competing hosted service.