MMDB
The Storage Engine Behind vsdb.
A pure-Rust LSM-Tree key-value storage engine, purpose-built for vsdb.
Scope — MMDB is designed and optimised exclusively for vsdb's workload: prefix-partitioned keys, bulk COW mutations, and compaction-driven garbage collection. While the API is generic enough for other key-value use cases, optimisation efforts focus solely on the vsdb scenario. If you need a general-purpose embedded KV store, consider RocksDB, sled, or redb.
Performance
In typical configurations, MMDB's scan throughput and point-read latency are comparable to RocksDB. The engine uses the same core optimizations (bloom filters, block cache, prefix compression, leveled compaction) and is designed to perform well across both warm-cache and small-block-cache workloads. Run make bench to evaluate performance under your specific hardware and data profile.
Features
| Feature | Status |
|---|---|
| Put / Get / Delete | Implemented |
| WriteBatch (atomic multi-key writes) | Implemented |
| WAL with group commit & crash recovery | Implemented |
| SST files with prefix-compressed blocks | Implemented |
| Bloom filters (per-key + prefix bloom) | Implemented |
| Block cache (moka LRU) with L0 first-block pinning (insert_pinned) | Implemented |
| Table cache (open file handles) | Implemented |
| MANIFEST version tracking + compaction | Implemented |
| Leveled compaction with trivial move | Implemented |
| Multi-threaded background compaction | Implemented |
| Streaming compaction (O(block) memory) | Implemented |
| MVCC snapshots via sequence numbers | Implemented |
| Forward/backward/prefix/range iterators | Implemented |
| Compression: None, LZ4, Zstd (per-level) | Implemented |
| Write backpressure (slowdown/stop) | Implemented |
| DeleteRange (range tombstones) | Implemented |
| CompactRange API (with range filtering) | Implemented |
| Compaction filter | Implemented |
| Rate limiter (token bucket, wired to compaction) | Implemented |
| DB properties/statistics (wired to all paths) | Implemented |
| Typed errors with full propagation traces | Implemented |
max_immutable_memtables is retained as an accepted-but-unused compatibility
option. MMDB flushes each frozen MemTable synchronously, so there is no
immutable-MemTable queue for that value to bound; other options are wired to
their documented behavior.
Architecture
+-------------+
| DB (API) | get / put / delete / write_batch
+------+------+
|
+------------+------------+
v v v
+----------+ +---------+ +-----------+
| MemTable | | WAL | | MANIFEST |
| (active) | | (append)| | (versions)|
+----+-----+ +---------+ +-----------+
| freeze
v
+----------+
| MemTable |
| (immut.) |
+----+-----+
| flush
v
+------------------------------------+
| SST Layer (L0 .. Ln) |
| L0: overlapping files |
| L1+: sorted, non-overlapping |
+------------------------------------+
^
| background compaction (N threads)
+----------+-----------+
| Leveled Compaction |
+----------------------+
Write Path
- Encode as InternalKey (
user_key + !pack(sequence, type)— inverted big-endian for descending sequence order) - Append to WAL (group commit: leader batches multiple writers into one fsync)
- Insert into active MemTable (concurrent skiplist)
- When MemTable exceeds
write_buffer_size, freeze and flush to L0 SST - Signal background compaction threads if L0 file count exceeds threshold
Read Path
- Active MemTable (newest writes)
- Immutable MemTables (newest first)
- L0 SST files (newest first, per-file bloom filter + range check)
- L1+ SST files (LevelIterator with lazy file opening, binary search by key range)
Iterator Architecture
The scan path uses a MergingIterator (min-heap) that merges entries from:
- MemTable cursors (O(1) per entry via skiplist level-0 chain)
- L0 TableIterators (one per overlapping SST file)
- LevelIterators (one per L1+ level — lazy two-level iterator)
Key optimizations:
- Single-source bypass: When only one source exists,
MergingIteratorbypasses heap machinery — direct delegation - Lock-free reads: Read path uses
ArcSwap<SuperVersion>snapshot instead of locking the inner mutex - Buffer-reusing iteration:
next_into()copies directly into caller buffers — minimizes heap allocation per entry - In-place key truncation: Truncates internal keys to user keys in-place instead of allocating
- Block cache:
BlockstoresArc<Vec<u8>>— cache hits avoid memcpy - Cursor-based block iteration: Decodes entries one-at-a-time from raw block data — no per-block Vec allocation
- Deferred block read: SST index stores
first_keyper block; Seek positions without reading data blocks - Sequential readahead:
posix_fadvise(WILLNEED)after detecting sequential block access - L0 first-block pinning: Each L0 file's first data block (smallest key) is pinned via
insert_pinnedsoinit_heap's firstpeek()is always a cache hit; unpinned on compaction. Index and filter blocks are held directly asTableReaderfields and never go throughblock_cache - Sweep-line range tombstone tracking: O(1) amortized per key
- Lazy index loading: Index parsed on first use
- Atomic L0 counter: Write-throttle checks use an atomic counter, avoiding mutex contention on writes
SST Format
Data Block 0: [entries with prefix compression + restart points]
Data Block 1: ...
...
Filter Block: [bloom filter bits]
Prefix Filter Block: [prefix bloom bits]
Metaindex Block: ["filter.bloom" -> handle, "filter.prefix" -> handle]
Index Block: [last_key -> BlockHandle + first_key per block]
Footer (48 bytes): [metaindex_handle, index_handle, magic]
Source Code Structure
src/
+-- lib.rs # Public API re-exports
+-- db.rs # DB: open/get/put/delete/write/flush/compact/close
+-- options.rs # DbOptions, ReadOptions, WriteOptions (RocksDB-compatible)
+-- types.rs # InternalKey, ValueType, SequenceNumber, WriteBatch
+-- error.rs # Error types
+-- rate_limiter.rs # Token-bucket rate limiter
+-- stats.rs # Database statistics
+-- memtable/
| +-- mod.rs # MemTable (put/get/iter with approximate_size tracking)
| +-- skiplist.rs # OrdInternalKey + skiplist + MemTableCursorIter (O(1) iteration)
| +-- skiplist_impl.rs # Lock-free skiplist implementation (raw pointers, arena allocation)
+-- wal/
| +-- writer.rs # WAL append with block-based fragmentation
| +-- reader.rs # WAL replay for crash recovery
| +-- record.rs # Record format: checksum + length + type + payload
+-- sst/
| +-- block.rs # Data block: prefix compression + restart points + seek
| +-- block_builder.rs # Block construction
| +-- table_builder.rs # SST writer (data + filter + index with first_key + footer)
| +-- table_reader/
| | +-- mod.rs # SST reader: open, block read/cache, point lookup
| | +-- iterator.rs # TableIterator (cursor-based, deferred block read)
| +-- filter.rs # Bloom filter (double hashing)
| +-- format.rs # Footer, BlockHandle, CompressionType, IndexEntry encoding
+-- compaction/
| +-- leveled.rs # Leveled compaction: streaming merge, trivial move, filter
+-- manifest/
| +-- version_edit.rs # VersionEdit encode/decode
| +-- version.rs # Version (immutable SST file set snapshot)
| +-- version_set.rs # VersionSet (MANIFEST management + version chain)
+-- cache/
| +-- block_cache.rs # Block LRU cache (moka) with L0 pinning support
| +-- table_cache.rs # SST reader cache
+-- iterator/
+-- merge.rs # Min-heap MergingIterator with buffer reuse + prefetch
+-- source.rs # IterSource (peeked-entry adapter) + SeekableIterator trait
+-- db_iter.rs # DBIterator: dedup, snapshot, tombstone/range-del filtering
+-- level_iter.rs # LevelIterator: lazy two-level iterator for L1+
+-- range_del.rs # FragmentedRangeTombstoneList (O(log T) lookup) + sweep-line RangeTombstoneTracker
+-- bidi_iter.rs # BidiIterator: bidirectional iteration
Selected Public API
The complete supported API is the set of crate-root re-exports documented on
docs.rs/mmdb. In addition to the selected methods
below, that includes BidiIterator, WriteBatchWithIndex, BlockCache,
BlockCachePool, CompressionType, compaction/property filter traits, and
the public size-limit constants.
Error Handling
All fallible APIs return mmdb::Result<T> = Result<T, mmdb::Error>.
Error carries a typed [ErrorKind] (Io, Corruption, InvalidArgument,
DbClosed, Background) for programmatic matching, plus a full propagation
trace — the origin file:line:column and one frame per .ctx() /
.with_ctx(..) hop — and preserves the underlying source error (e.g.
std::io::Error) for std::error::Error::source() downcasting.
Display/Debug render the complete chain:
Corruption: WAL 000012 CRC mismatch
at src/wal/reader.rs:116:38
at src/db.rs:410:36 -- refusing prefix recovery
caused by: unexpected end of file
match db.get
Error is Clone + Send + Sync, so one failure can be shared across
group-commit waiters without stringification.
Configuration
let opts = DbOptions ;
// Or use a preset profile:
let opts = write_heavy; // 128MB memtable, 4 compaction threads, LZ4
let opts = read_heavy; // large cache, 14 bits/key bloom
Feature Comparison
See COMPARISON.md for a detailed feature-by-feature comparison with RocksDB and Pebble, including gap analysis and recommendations.
Migrating 3.x → 4.0
v4.0 is a design-level cleanup release. Breaking changes:
- Typed errors replace
rucchains.Result<T>is nowResult<T, mmdb::Error>; match onerr.kind()(ErrorKind). Errors still carry a full per-hopfile:line:columnpropagation trace (rendered byDisplay/Debug), plusstd::error::Error::source(),Clone, andSync— a strict superset of the oldBox<dyn RucError>payload. Code that only propagates/prints errors (incl.ruc's.c(d!())on the caller side) keeps working sinceError: Display + Debug + Send + Sync. - Internal modules are private. Import everything from the crate root
(e.g.
mmdb::CompressionType, notmmdb::sst::format::CompressionType). - No-op "compatibility" options removed from
DbOptions(cache_index_and_filter_blocks,max_write_buffer_number,level_compaction_dynamic_level_bytes,allow_concurrent_memtable_write,memtable_prefix_bloom_ratio),ReadOptions(verify_checksums,readahead_size,total_order_seek,pin_data) andWriteOptions(low_pri). Delete them from struct literals.ReadOptions::fill_cacheremains and is now actually implemented (checksums are always verified). - Iterator pool removed.
pool_return()andPooledIteratorare gone — just drop iterators. - RAII-only snapshots.
snapshot_seq()/release_snapshot()are private; uselet snap = db.snapshot();andsnap.read_options()/snap.sequence(). write_with_options(&WriteOptions, WriteBatch)— options now come first, consistent with every other*_with_optionsmethod.
Build & Test
Benchmarks cover warm-cache (256MB block cache, data in memory) and small-cache (256KB block cache, block-cache-miss plus decode overhead) scenarios. Small-cache reads still use the same-process OS page cache, so they do not measure storage latency; those cases use smaller datasets to keep runtime reasonable.
License
MIT