https://github.com/jamesgober/txn-db
MVCC transaction primitive for Rust databases - snapshot isolation, SSI, deadlock detection.
https://github.com/jamesgober/txn-db
database mvcc reps rust snapshot-isolation transactions
Last synced: 2 days ago
JSON representation
MVCC transaction primitive for Rust databases - snapshot isolation, SSI, deadlock detection.
- Host: GitHub
- URL: https://github.com/jamesgober/txn-db
- Owner: jamesgober
- License: apache-2.0
- Created: 2026-05-29T23:47:59.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-06T19:01:11.000Z (about 1 month ago)
- Last Synced: 2026-06-06T21:04:10.661Z (about 1 month ago)
- Topics: database, mvcc, reps, rust, snapshot-isolation, transactions
- Language: Rust
- Size: 63.5 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE-APACHE
Awesome Lists containing this project
README
txn-db
MVCC TRANSACTION ENGINE
txn-db is a multi-version concurrency control transaction engine: the layer that turns a key-value store into a transactional database. Each write produces a new version tagged with a commit timestamp, so readers get a stable snapshot without ever blocking writers, and writers detect conflicts at commit time rather than holding locks for the duration of a transaction.
It is deliberately a layer, not a store: the version store is a trait, so txn-db composes on top of lsm-db or any other backing store, and its durable commit log is wal-db. Snapshot isolation is the default; serializable isolation (SSI) is a feature flag.
The common case is begin / get / put / commit, with automatic conflict detection and retry guidance on the error path.
MSRV is 1.85+ (Rust 2024 edition). Snapshot isolation by default. Optional serializable (SSI). Durable commits via wal-db.
Status: stable (1.0). The public API is frozen until 2.0, and the durable commit-log format is frozen for the 1.x series (see docs/COMMIT_LOG_FORMAT.md). See CHANGELOG.md for detail.
What it does
Available now (`0.5`, feature-complete):
- **MVCC** — each write creates a new version; readers see a consistent snapshot without blocking writers
- **Snapshot isolation** — a transaction reads the database as of its start timestamp; its own writes are visible to itself before commit
- **Serializable (SSI)** — opt-in read-set validation under the `serializable` feature, rejecting write skew and the read-only anomaly
- **Durable commit log** — under the `durability` feature, `Db::open` logs each commit to a `wal-db` write-ahead log and syncs before acknowledging; the log is replayed on restart
- **Garbage collection** — `Db::collect_garbage` reclaims versions no live transaction or snapshot can observe; an oldest-reader watermark guarantees a held snapshot's versions are never reclaimed
- **Write-write conflict detection** — first-committer-wins at commit; the later writer is told to retry with a typed, retryable error
- **Sharded commit path** — lock-free timestamp allocation and per-shard conflict checks, so commits to unrelated keys do not contend (loom-checked)
- **Pluggable backing store** — the version store is the `VersionStore` trait; an in-memory store ships, and any backend (an LSM tree, a B-tree, a remote store) plugs in unchanged
## Installation
```toml
[dependencies]
txn-db = "1.0"
# Opt into serializable isolation and/or a durable commit log:
txn-db = { version = "1.0", features = ["serializable", "durability"] }
```
## Quick start
For a single read or write, skip the ceremony — `get`, `put`, and `delete` on
the database run in their own transaction (writes retry on conflict):
```rust
use txn_db::Db;
let db = Db::new();
db.put(b"user:1:name".to_vec(), b"ada".to_vec())?;
assert_eq!(db.get(b"user:1:name")?.as_deref(), Some(&b"ada"[..]));
# Ok::<(), txn_db::TxnError>(())
```
When several operations must be atomic, open a transaction: begin, read and
write through it, commit.
```rust
use txn_db::Db;
let db = Db::new();
// Write two keys in one atomic transaction.
let mut tx = db.begin();
tx.put(b"user:1:name".to_vec(), b"ada".to_vec());
tx.put(b"user:1:role".to_vec(), b"admin".to_vec());
tx.commit()?;
// A later transaction reads a consistent snapshot.
let tx = db.begin();
assert_eq!(tx.get(b"user:1:name")?.as_deref(), Some(&b"ada"[..]));
# Ok::<(), txn_db::TxnError>(())
```
When two transactions race to write the same key, the first to commit wins and
the second is told to retry — that is what prevents lost updates:
```rust
use txn_db::Db;
let db = Db::new();
let mut a = db.begin();
let mut b = db.begin();
a.put(b"counter".to_vec(), b"1".to_vec());
b.put(b"counter".to_vec(), b"2".to_vec());
a.commit()?; // first committer wins
let err = b.commit().unwrap_err(); // second is rejected
assert!(err.is_retryable()); // retry against the fresh snapshot
# Ok::<(), txn_db::TxnError>(())
```
The retry loop is a few lines; see [`examples/concurrent_counter.rs`](./examples/concurrent_counter.rs)
for the contended read-modify-write pattern, [`examples/bank_transfer.rs`](./examples/bank_transfer.rs)
for an atomic multi-key transfer, and [`examples/custom_store.rs`](./examples/custom_store.rs)
for plugging in your own `VersionStore`.
## Serializable isolation
Snapshot isolation still allows *write skew*: two transactions that read the same
rows and write different ones can both commit, breaking an invariant that ties
those rows together. With the `serializable` feature,
[`begin_serializable`](https://docs.rs/txn-db) validates a transaction's read set
at commit and rejects exactly those cases.
```rust
# #[cfg(feature = "serializable")]
# {
use txn_db::Db;
let db = Db::new();
let mut seed = db.begin();
seed.put(b"on_call:alice".to_vec(), vec![1]);
seed.put(b"on_call:bob".to_vec(), vec![1]);
seed.commit()?;
// Both read the pair, then each takes one row off — classic write skew.
let mut t1 = db.begin_serializable();
let mut t2 = db.begin_serializable();
let _ = (t1.get(b"on_call:alice")?, t1.get(b"on_call:bob")?);
let _ = (t2.get(b"on_call:alice")?, t2.get(b"on_call:bob")?);
t1.put(b"on_call:alice".to_vec(), vec![0]);
t2.put(b"on_call:bob".to_vec(), vec![0]);
t1.commit()?; // first commits
assert!(t2.commit().is_err()); // second read a row t1 changed — rejected
# }
# Ok::<(), txn_db::TxnError>(())
```
See [`examples/serializable_doctors.rs`](./examples/serializable_doctors.rs) for the
full on-call-doctors demonstration, side by side under both isolation levels.
## Durability
With the `durability` feature, `Db::open` backs the database with a `wal-db`
write-ahead log. Each commit's record is appended and synced before `commit`
returns, so an acknowledged commit survives a crash; on restart the log is
replayed and uncommitted work leaves no trace.
```rust
# #[cfg(feature = "durability")]
# {
# let dir = tempfile::tempdir().unwrap();
# let path = dir.path().join("txn.wal");
use txn_db::Db;
// First run: commit, then the process exits.
{
let db = Db::open(&path)?;
let mut tx = db.begin();
tx.put(b"k".to_vec(), b"v".to_vec());
tx.commit()?;
}
// Restart: the log is replayed and the committed write is back.
let db = Db::open(&path)?;
assert_eq!(db.begin().get(b"k")?.as_deref(), Some(&b"v"[..]));
# }
# Ok::<(), txn_db::TxnError>(())
```
See [`examples/durable_store.rs`](./examples/durable_store.rs) for a commit /
drop / reopen walkthrough.
## Garbage collection
Every write keeps the previous version so in-flight readers see a stable
snapshot, so versions accumulate. `Db::collect_garbage` reclaims the versions no
live transaction or snapshot can still observe and returns how many it removed.
A held snapshot pins the versions it can see, so collection never reclaims data
a live reader depends on.
```rust
use txn_db::Db;
let db = Db::new();
for v in 0..100u8 {
let mut tx = db.begin();
tx.put(b"k".to_vec(), vec![v]);
tx.commit()?;
}
// With no snapshot held, only the newest version need be kept.
let reclaimed = db.collect_garbage();
assert!(reclaimed > 0);
assert_eq!(db.begin().get(b"k")?.as_deref(), Some(&[99u8][..]));
# Ok::<(), txn_db::TxnError>(())
```
See [`examples/garbage_collection.rs`](./examples/garbage_collection.rs) for a
demonstration of a held snapshot pinning versions against collection.
## Examples
| Example | What it shows |
|---------|---------------|
| [`quick_start`](./examples/quick_start.rs) | Shortest end-to-end: open, write, read back. |
| [`bank_transfer`](./examples/bank_transfer.rs) | Atomic multi-key update with conflict retries. |
| [`concurrent_counter`](./examples/concurrent_counter.rs) | Many threads increment one key; no update is lost. |
| [`snapshot_reads`](./examples/snapshot_reads.rs) | A snapshot stays stable as the database moves on. |
| [`custom_store`](./examples/custom_store.rs) | Backing the engine with a custom `VersionStore`. |
| [`serializable_doctors`](./examples/serializable_doctors.rs) | Write skew under SI vs serializable (needs `--features serializable`). |
| [`durable_store`](./examples/durable_store.rs) | Commit, drop, reopen — recovery from the log (needs `--features durability`). |
| [`garbage_collection`](./examples/garbage_collection.rs) | Reclaiming old versions; a held snapshot pins what it can see. |
```bash
cargo run --example quick_start
cargo run --example garbage_collection
cargo run --example serializable_doctors --features serializable
cargo run --example durable_store --features durability
```
## Status
This is `1.0` — **stable**. The engine is feature-complete (snapshot and
serializable isolation, sharded lock-free commits, a durable `wal-db` commit log,
watermark garbage collection), tuned, hardened against adversarial schedules, and
benchmarked honestly. The **public API is frozen until `2.0`** and the durable
commit-log format is frozen for the `1.x` series
([`docs/COMMIT_LOG_FORMAT.md`](./docs/COMMIT_LOG_FORMAT.md)). See
[`docs/API.md`](./docs/API.md) for the full surface and
[`docs/PERFORMANCE.md`](./docs/PERFORMANCE.md) for hot-path and comparison
numbers.
## Where It Fits
`txn-db` is the transaction layer. It builds on:
- [`wal-db`](https://github.com/jamesgober/wal-db) — durable transaction commit log
- [`lsm-db`](https://github.com/jamesgober/lsm-db) — a natural backing version store
- Hive DB — the transaction orchestration layer (DISTRO) builds on these semantics
It stays foreign-compatible: usable standalone over any version store that implements the trait.
## Contributing
Before opening a PR, `cargo fmt --all`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test --all-features` must be clean. Hot-path changes require a `criterion` benchmark; correctness-critical paths require property and/or `loom` tests.
License
Licensed under either of
-
Apache License, Version 2.0 — see LICENSE-APACHE
-
MIT License — see LICENSE-MIT
at your option.
COPYRIGHT © 2026 JAMES GOBER.