Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,27 @@ working, or if the notes become outdated.

Rust bindings for libmdbx (MDBX database). Crate name: `signet-libmdbx`.

## MDBX Synchronization Model

When making changes to this codebase you MUST remember and conform to the MDBX
synchronization model for transactions and cursors. Access to raw pointers MUST
be mediated via the `TxAccess` trait. The table below summarizes the
transaction types and their access models.

| Transaction Type | Thread Safety | Access Model | enforced by Rust type system? |
| ---------------- | ------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Read-Only (RO) | !Sync + Send | Access MUST be totally ordered and non-concurrent | No |
| Read-Write (RW) | !Sync + !Send | Access MUST be totally ordered, and non-concurrent. Only the creating thread may manage TX lifecycle | No |
| Transaction<K> | Sync + Send | Multi-threaded wrapper using Mutex and Arc to share a RO or RW transaction safely across threads | Yes, via synchronization wrappers |
| TxUnsync<RO> | !Sync + Send | Single-threaded RO transaction without synchronization overhead | Yes, via required &mut or & access |
| TxUnsync<RW> | !Sync + !Send | Single-threaded RW transaction without synchronization overhead | Yes, &self enforces via required ownership and !Send + !Sync bound |
| Cursors | [Inherited] | Cursors borrow a Tx. The cursor CANNOT outlive the tx, and must reap its pointer on drop | Yes, via lifetimes |

## Key Types

- `Environment` - Database environment (in `src/sys/environment.rs`)
- `Transaction<K>` - Transaction with kind marker RO/RW (in `src/tx/transaction.rs`)
- `TxSync<K>` - Transaction with kind marker RO/RW (in `src/tx/sync.rs`)
- `TxUnsync<K>` - Unsynchronized transaction with kind marker RO/RW (in `src/tx/unsync.rs`)
- `Database` - Handle to a database, stores `dbi` + `DatabaseFlags` (in `src/tx/database.rs`)
- `Cursor<'tx, K>` - Database cursor, stores `&Transaction`, raw cursor ptr, and `Database` (in `src/tx/cursor.rs`)

Expand Down Expand Up @@ -53,7 +70,8 @@ src/
mod.rs
cursor.rs - Cursor impl
database.rs - Database struct
transaction.rs - Transaction impl
sync.rs - Transaction impl
unsync.rs - Unsynchronized transaction impl
iter.rs - Iterator types
sys/
environment.rs - Environment impl
Expand All @@ -63,6 +81,9 @@ tests/
environment.rs - Environment tests
benches/
cursor.rs - Cursor benchmarks
transaction.rs - Transaction benchmarks
db_open.rs - Database open benchmarks
utils.rs - Benchmark utilities
```

## Testing
Expand All @@ -74,3 +95,16 @@ cargo clippy --all-features --all-targets
cargo clippy --no-default-features --all-targets
cargo +nightly fmt
```

## Linux Testing

Before committing, run the Docker Linux environment to verify changes pass on Linux:

```bash
docker build -t mdbx-linux-tests . && docker run --rm mdbx-linux-tests
```

This SHOULD be run alongside local tests and linting, especially for changes that:
- Modify build configuration
- Add new dependencies
- Change platform-specific code
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "signet-libmdbx"
description = "Idiomatic and safe MDBX wrapper"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
rust-version = "1.92"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -49,6 +49,7 @@ read-tx-timeouts = ["dep:dashmap"]

[dev-dependencies]
criterion = "0.8.1"
proptest = "1"
rand = "0.9.2"
tempfile = "3.20.0"

Expand Down
26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM ubuntu:24.04

# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install Rust (latest stable)
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"

# Install nightly for rustfmt
RUN rustup toolchain install nightly --component rustfmt

WORKDIR /work

# Copy source
COPY . .

# Default: run full checks (fmt, clippy, tests)
CMD ["sh", "-c", "cargo +nightly fmt --check && \
cargo clippy --all-features --all-targets -- -D warnings && \
cargo clippy --no-default-features --all-targets -- -D warnings && \
cargo test --all-features && \
cargo test --no-default-features"]
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ NOTE: Most of the repo came from [lmdb-rs bindings].
## Differences from reth-libmdbx

- Improve documentation :)
- Add [`TxUnsync`] type for single-threaded transactions.
- These may be up to 3x faster than the thread-safe versions.
- Rename [`Transaction`] to [`TxSync`] for clarity.
- Improve support for custom `TableObject` types.
- Added `TableObjectOwned` trait to represent types that can be deserialized
from a database table without borrowing.
Expand Down Expand Up @@ -49,6 +52,22 @@ cp -R ../libmdbx/dist mdbx-sys/libmdbx
git add mdbx-sys/libmdbx
```

## Linux Testing

Run tests in a Linux environment (Ubuntu 24.04):

```bash
# Build the test image
docker build -t mdbx-linux-tests .

# Run full checks (fmt, clippy, tests)
docker run --rm mdbx-linux-tests

# Run specific commands
docker run --rm mdbx-linux-tests cargo test --all-features
docker run --rm mdbx-linux-tests cargo clippy --all-features --all-targets
```

[libmdbx]: https://github.com/erthink/libmdbx
[reth-libmdbx]: https://github.com/paragidmxyz/reth
[building steps]: https://github.com/erthink/libmdbx#building
Expand Down
86 changes: 80 additions & 6 deletions benches/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
mod utils;

use criterion::{Criterion, criterion_group, criterion_main};
use signet_libmdbx::{ffi::*, *};
use signet_libmdbx::{Cursor, ObjectLength, ReadResult, TransactionKind, ffi::*, tx::TxPtrAccess};
use std::{hint::black_box, ptr};
use utils::*;

Expand All @@ -15,7 +15,7 @@ fn bench_get_seq_iter(c: &mut Criterion) {
// Note: setup_bench_db creates a named database which adds metadata to the
// main database, so actual item count is n + 1
let actual_items = n + 1;
c.bench_function("bench_get_seq_iter", |b| {
c.bench_function("cursor::traverse::iter_x3", |b| {
b.iter(|| {
let mut cursor = txn.cursor(db).unwrap();
let mut i = 0;
Expand All @@ -34,7 +34,9 @@ fn bench_get_seq_iter(c: &mut Criterion) {
count += 1;
}

fn iterate<K: TransactionKind>(cursor: &mut Cursor<K>) -> ReadResult<()> {
fn iterate<K: TransactionKind, A: TxPtrAccess>(
cursor: &mut Cursor<K, A>,
) -> ReadResult<()> {
let mut i = 0;
for result in cursor.iter::<ObjectLength, ObjectLength>() {
let (key_len, data_len) = result?;
Expand All @@ -60,7 +62,78 @@ fn bench_get_seq_cursor(c: &mut Criterion) {
// Note: setup_bench_db creates a named database which adds metadata to the
// main database, so actual item count is n + 1
let actual_items = n + 1;
c.bench_function("bench_get_seq_cursor", |b| {
c.bench_function("cursor::traverse::iter", |b| {
b.iter(|| {
let (i, count) = txn
.cursor(db)
.unwrap()
.iter::<ObjectLength, ObjectLength>()
.map(Result::unwrap)
.fold((0, 0), |(i, count), (key, val)| (i + *key + *val, count + 1));

black_box(i);
assert_eq!(count, actual_items);
})
});
}

/// Benchmark of iterator sequential read performance (single-thread).
fn bench_get_seq_iter_single_thread(c: &mut Criterion) {
let n = 100;
let (_dir, env) = setup_bench_db(n);
let mut txn = env.begin_ro_unsync().unwrap();
let db = txn.open_db(None).unwrap();
// Note: setup_bench_db creates a named database which adds metadata to the
// main database, so actual item count is n + 1
let actual_items = n + 1;
c.bench_function("cursor::traverse::iter_x3::single_thread", |b| {
b.iter(|| {
let mut cursor = txn.cursor(db).unwrap();
let mut i = 0;
let mut count = 0u32;

for (key_len, data_len) in
cursor.iter::<ObjectLength, ObjectLength>().map(Result::unwrap)
{
i = i + *key_len + *data_len;
count += 1;
}
for (key_len, data_len) in
cursor.iter::<ObjectLength, ObjectLength>().filter_map(Result::ok)
{
i = i + *key_len + *data_len;
count += 1;
}

fn iterate<K: TransactionKind, A: TxPtrAccess>(
cursor: &mut Cursor<K, A>,
) -> ReadResult<()> {
let mut i = 0;
for result in cursor.iter::<ObjectLength, ObjectLength>() {
let (key_len, data_len) = result?;
i = i + *key_len + *data_len;
}
Ok(())
}
iterate(&mut cursor).unwrap();

black_box(i);
// Both loops iterate all items since iter() repositions exhausted cursors
assert_eq!(count, actual_items * 2);
})
});
}

/// Benchmark of cursor sequential read performance (single-thread).
fn bench_get_seq_cursor_single_thread(c: &mut Criterion) {
let n = 100;
let (_dir, env) = setup_bench_db(n);
let mut txn = env.begin_ro_unsync().unwrap();
let db = txn.open_db(None).unwrap();
// Note: setup_bench_db creates a named database which adds metadata to the
// main database, so actual item count is n + 1
let actual_items = n + 1;
c.bench_function("cursor::traverse::iter::single_thread", |b| {
b.iter(|| {
let (i, count) = txn
.cursor(db)
Expand Down Expand Up @@ -91,7 +164,7 @@ fn bench_get_seq_raw(c: &mut Criterion) {
// main database, so actual item count is n + 1
let actual_items = n + 1;

c.bench_function("bench_get_seq_raw", |b| {
c.bench_function("cursor::traverse::raw", |b| {
b.iter(|| unsafe {
txn.txn_execute(|txn| {
mdbx_cursor_open(txn, dbi, &raw mut cursor);
Expand All @@ -115,6 +188,7 @@ fn bench_get_seq_raw(c: &mut Criterion) {
criterion_group! {
name = benches;
config = Criterion::default();
targets = bench_get_seq_iter, bench_get_seq_cursor, bench_get_seq_raw
targets = bench_get_seq_iter, bench_get_seq_cursor, bench_get_seq_raw,
bench_get_seq_iter_single_thread, bench_get_seq_cursor_single_thread
}
criterion_main!(benches);
55 changes: 46 additions & 9 deletions benches/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn bench_get_rand(c: &mut Criterion) {
let mut keys: Vec<String> = (0..n).map(get_key).collect();
keys.shuffle(&mut StdRng::from_seed(Default::default()));

c.bench_function("bench_get_rand", |b| {
c.bench_function("transaction::get::rand", |b| {
b.iter(|| {
let mut i = 0usize;
for key in &keys {
Expand All @@ -41,7 +41,7 @@ fn bench_get_rand_raw(c: &mut Criterion) {
let mut key_val: MDBX_val = MDBX_val { iov_len: 0, iov_base: ptr::null_mut() };
let mut data_val: MDBX_val = MDBX_val { iov_len: 0, iov_base: ptr::null_mut() };

c.bench_function("bench_get_rand_raw", |b| {
c.bench_function("transaction::get::rand::raw", |b| {
b.iter(|| unsafe {
txn.txn_execute(|txn| {
let mut i = 0;
Expand All @@ -64,17 +64,15 @@ fn bench_put_rand(c: &mut Criterion) {
let n = 100u32;
let (_dir, env) = setup_bench_db(0);

let txn = env.begin_ro_txn().unwrap();
let db = txn.open_db(None).unwrap();

let mut items: Vec<(String, String)> = (0..n).map(|n| (get_key(n), get_data(n))).collect();
items.shuffle(&mut StdRng::from_seed(Default::default()));

c.bench_function("bench_put_rand", |b| {
c.bench_function("transaction::put::rand", |b| {
b.iter(|| {
let txn = env.begin_rw_txn().unwrap();
let db = txn.open_db(None).unwrap();
for (key, data) in &items {
txn.put(db.dbi(), key, data, WriteFlags::empty()).unwrap();
txn.put(db, key, data, WriteFlags::empty()).unwrap();
}
})
});
Expand All @@ -92,7 +90,7 @@ fn bench_put_rand_raw(c: &mut Criterion) {
let mut key_val: MDBX_val = MDBX_val { iov_len: 0, iov_base: ptr::null_mut() };
let mut data_val: MDBX_val = MDBX_val { iov_len: 0, iov_base: ptr::null_mut() };

c.bench_function("bench_put_rand_raw", |b| {
c.bench_function("transaction::put::rand::raw", |b| {
b.iter(|| unsafe {
let mut txn: *mut MDBX_txn = ptr::null_mut();
env.with_raw_env_ptr(|env| {
Expand All @@ -114,9 +112,48 @@ fn bench_put_rand_raw(c: &mut Criterion) {
});
}

fn bench_get_rand_unsync(c: &mut Criterion) {
let n = 100u32;
let (_dir, env) = setup_bench_db(n);
let mut txn = env.begin_ro_unsync().unwrap();
let db = txn.open_db(None).unwrap();

let mut keys: Vec<String> = (0..n).map(get_key).collect();
keys.shuffle(&mut StdRng::from_seed(Default::default()));

c.bench_function("transaction::get::rand::single_thread", |b| {
b.iter(|| {
let mut i = 0usize;
for key in &keys {
i += *txn.get::<ObjectLength>(db.dbi(), key.as_bytes()).unwrap().unwrap();
}
black_box(i);
})
});
}

fn bench_put_rand_unsync(c: &mut Criterion) {
let n = 100u32;
let (_dir, env) = setup_bench_db(0);

let mut items: Vec<(String, String)> = (0..n).map(|n| (get_key(n), get_data(n))).collect();
items.shuffle(&mut StdRng::from_seed(Default::default()));

c.bench_function("transaction::put::rand::single_thread", |b| {
b.iter(|| {
let mut txn = env.begin_rw_unsync().unwrap();
let db = txn.open_db(None).unwrap();
for (key, data) in &items {
txn.put(db, key, data, WriteFlags::empty()).unwrap();
}
})
});
}

criterion_group! {
name = benches;
config = Criterion::default();
targets = bench_get_rand, bench_get_rand_raw, bench_put_rand, bench_put_rand_raw
targets = bench_get_rand, bench_get_rand_raw, bench_put_rand, bench_put_rand_raw,
bench_get_rand_unsync, bench_put_rand_unsync
}
criterion_main!(benches);
4 changes: 2 additions & 2 deletions benches/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ pub fn setup_bench_db(num_rows: u32) -> (TempDir, Environment) {
let txn = env.begin_rw_txn().unwrap();
let db = txn.open_db(None).unwrap();
for i in 0..num_rows {
txn.put(db.dbi(), get_key(i), get_data(i), WriteFlags::empty()).unwrap();
txn.put(db, get_key(i), get_data(i), WriteFlags::empty()).unwrap();
}

let named_db = txn.create_db(Some(NAMED_DB), Default::default()).unwrap();
for i in 0..num_rows {
txn.put(named_db.dbi(), get_key(i), get_data(i), WriteFlags::empty()).unwrap();
txn.put(named_db, get_key(i), get_data(i), WriteFlags::empty()).unwrap();
}
txn.commit().unwrap();
}
Expand Down
Loading