txmap 0.1.0

A concurrent transactional hash map with fine-grained locking and internal mutability
Documentation

TxMap

crates.io docs.rs MIT License

A concurrent transactional hash map for Rust with fine-grained locking and internal mutability.

TxMap partitions stored key-value pairs across multiple shards, each protected by it's own parking_lot::Mutex and backed by it's own hashbrown:HashMap. Read and write operations acquire locks only on the shards they need, maximizing concurrency. Transactional operations group multiple operations into atomic units, and support parameterized closures.

Features

  • Concurrent access Fine-grained shard-level locking; operations lock only the shards they touch
  • Transactions Atomic, composable batches of modifications
  • Optional parameterized transactions Optionally define a parameter type to pass into transaction closures
  • Guards/conditions Declarative preconditions that must hold before a transaction runs
  • Flexible operations Modify, map, insert, remove, swap, move, retain, and more
  • Builder API Chain operations to build transactions with a fluent interface
  • No unsafe 100% safe Rust

License

Licensed under the MIT License.

Usage

Add txmap to your Cargo.toml:

[dependencies]
txmap = "0.1.0"

Creating a TxMap

use txmap::prelude::*;

// Choose a shard count, a power of two between 8 to 128 inclusive
let map: TxMap<String, u64> = TxMap::new(ShardCount::_8);

Larger shard counts reduce lock contention at the cost of slightly more memory. Choose the smallest count that gives adequate concurrency for your workload.

Key type requirements

The map key type K must implement Hash and Eq. Some functions also require Clone. The value type V has no trait bounds by default. Operations that create default values (e.g., modify_or_default) require V: Default.

Basic operations

use txmap::prelude::*;

let map = TxMap::new(ShardCount::_8);

// Insert
map.insert("alice".to_string(), 100u64);

// Get a value with a transform closure. Direct access to values is not supported, this is to enable concurrent modification
let balance = map.get_with(&"alice".to_string(), |v| *v);
assert_eq!(balance, Some(100));

// Remove
let old = map.remove(&"alice".to_string());
assert_eq!(old, Some(100));

// Clear all entries
map.clear();

// Length / empty
assert!(map.is_empty());
assert_eq!(map.len(), 0);

Transactions

Transactions group multiple operations into an atomic unit. They are built using a fluent builder API.

Simple transaction (no result)

use txmap::prelude::*;

let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);

db.insert("alice".to_string(), 100);

// Transfer 50 from alice to bob in one transaction
db.transaction()
    .modify("alice".to_string(), |_name, balance| {
        *balance -= 50;
    })
    .modify_or_default("bob".to_string(), |_name, balance| {
        *balance += 50;
    })
    .into_transaction()
    .execute();

assert_eq!(db.get_with(&"alice".to_string(), |v| *v), Some(50));
assert_eq!(db.get_with(&"bob".to_string(), |v| *v), Some(50));

Transaction with guards (preconditions)

use txmap::prelude::*;

let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);

let tx = db
    .transaction()
    .require(
        "Alice has sufficient funds",
        ["alice".to_string()],
        |[alice_balance]| alice_balance.is_some_and(|b| *b >= 50),
    )
    .modify("alice".to_string(), |_name, balance| {
        *balance -= 50;
    })
    .modify_or_default("bob".to_string(), |_name, balance| {
        *balance += 50;
    })
    .into_transaction();

match tx.execute() {
    TxResult::Completed(()) => println!("Transfer succeeded"),
    TxResult::RequirementNotMet(_, name) => {
        println!("Transaction rejected: {name}")
    }
}

If any guard condition fails, the transaction is not executed - no locks are held, no mutations occur - and TxResult::RequirementNotMet is returned.

Transaction returning a value

use txmap::prelude::*;

let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);

let new_balance = db
    .transaction()
    .modify("alice".to_string(), |_name, balance| {
        *balance += 50;
    })
    .get("alice".to_string(), |_name, balance| *balance)
    .into_transaction()
    .execute();

assert_eq!(new_balance, TxResult::Completed(Some(150)));

Transaction returning multiple values

use txmap::prelude::*;

let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);
db.insert("bob".to_string(), 200);

let balances = db
    .transaction()
    .get_all(
        vec!["alice".to_string(), "bob".to_string()],
        |_name, balance| *balance,
    )
    .into_transaction()
    .execute();

assert_eq!(balances, TxResult::Completed(vec![Some(100), Some(200)]));

Parameterized transactions

Parameterized transactions let you pass a parameter struct to all closures. This is useful for reusable transaction logic.

use txmap::prelude::*;

#[derive(Default)]
struct Transfer {
    amount: u64,
}

let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 200);

let transfer_alice_to_bob_tx = db
    .transaction()
    .with_param::<Transfer>()
    .require(
        "Alice has sufficient funds",
        ["alice".to_string()],
        |[balance], params| balance.is_some_and(|b| *b >= params.amount),
    )
    .modify("alice".to_string(), |_name, balance, params| {
        *balance -= params.amount;
    })
    .modify_or_default("bob".to_string(), |_name, balance, params| {
        *balance += params.amount;
    })
    .get_all(["alice".to_string(), "bob".to_string()], |_name, balance| *balance)
    .into_transaction();

// Execute with different parameters
let result1 = transfer_alice_to_bob_tx.execute(&Transfer { amount: 50 });
assert_eq!(result1, TxResult::Completed([Some(150), Some(50)]));

let result2 = transfer_alice_to_bob_tx.execute(&Transfer { amount: 30 });
assert_eq!(result2, TxResult::Completed([Some(120), Some(80)]));

Operation reference

Builder method Description Additional required bounds
insert_default Insert V::default() for the key. K: Clone V: Default
insert_default_if_absent Insert V::default() for the key, only if the key is absent. K: Clone V: Default
insert_with Insert a value generated from the key. K: Clone
insert_with_if_absent Insert a value generated from the key, only if the key is absent. K: Clone
modify Mutate an existing value in-place. Does nothing if key absent. V: Default
modify_peek Like modify while peeking at other values. K: Clone
update Update a single entry. Return Some(v) to insert/replace, None to delete. K: Clone
update_peek Like update while peeking at other values. K: Clone
move_value Remove a value from one key and insert it with another key. K: Clone
swap_value Swap the values of two keys. K: Clone
remove Remove the given keys.
remove_where Remove the given keys which also satisfy a condition.
retain_only Retain only the given keys.
retain_where Retain only the given keys which also satisfy a condition.
clear Remove all entries.
remove_if Remove any entries which satisfy a condition.
retain Retain only the entries which satisfy a condition.

Finisher methods

Up to one of these is called before into_transaction() in the builder chain to define what the transaction should return.

Method Description Transaction result type
(none - default) Execute with no return value. TxResult<()>
get_copied(key) Copy a single value. Requires V: Copy TxResult<Option<V>>
get_cloned(key) Clone a single value. Requires V: Clone TxResult<Option<V>>
get(key, |k, v[, params]| { ... }) Read a single value and apply a transformation to it. TxResult<Option<R>>
get_all(keys, |k, v[, params]| { ... }) Read multiple values and apply a transformation to them. TxResult<Vec<Option<R>>>

To create the final transaction call into_transaction(). This will produce a re-useable transaction that can be executed as many times as you want within the lifetime of it's TxMap.

TxResult

All transactions return TxResult<T>:

pub enum TxResult<T> {
    Completed(T),
    RequirementNotMet(usize, String),
}
  • Completed(result) The transaction was executed successfully.
  • RequirementNotMet(index, name) A guard condition failed; the transaction was aborted. The index indicates which guard failed, and name is the user-supplied description.

Operation appendix

Operation
insert_default(key)
insert_default_if_absent(key)
insert_with(key, |k[, params]| { new_value } )
insert_with_if_absent(key, |k[, params]| { new_value } )
modify(key, |k, mut v[, params]|)
modify_peek(key, peek_keys, |k, mut v, pks[, params]|)
update(key, |k, v_opt[, params]| { new_value_opt })
update_peek(key, peek_keys, |k, v_opt, pks[, params]| { new_value_opt })
move_value(from, to)
swap_value(a, b)
remove(keys)
remove_where(keys, |k, v[, params]| { remove })
retain_only(keys)
retain_where(keys, |k, v[, params]| { remove })
clear()
remove_if(|k, v[, params]| { remove })
retain(|k, v[, params]| { remove })