pub struct LockManager { /* private fields */ }std only.Expand description
A sharded lock table mapping resources to the transactions that hold them.
LockManager is the primary entry point of the crate. It is Send + Sync
and is meant to be shared behind an std::sync::Arc across all worker
threads; every method takes &self, so no outer lock is needed.
§Examples
use lock_db::{LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let row = ResourceId::new(100);
let (t1, t2) = (TxnId::new(1), TxnId::new(2));
// Two transactions read the same row concurrently.
lm.try_acquire(t1, row, LockMode::Shared).unwrap();
lm.try_acquire(t2, row, LockMode::Shared).unwrap();
assert_eq!(lm.holder_count(row), 2);
// Neither can take it exclusively while the other reads.
assert!(lm.try_acquire(t1, row, LockMode::Exclusive).is_err());
// After both release, an exclusive lock is free to take.
lm.release(t1, row).unwrap();
lm.release(t2, row).unwrap();
lm.try_acquire(t1, row, LockMode::Exclusive).unwrap();Implementations§
Source§impl LockManager
impl LockManager
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates a lock manager with a shard count chosen for the current machine.
The count scales with the number of available CPUs (rounded up to a power
of two) so that contention on any single shard mutex stays low on
multi-core systems. Use with_shards to pin an
exact count, for example in tests or on memory-constrained targets.
§Examples
use lock_db::LockManager;
let lm = LockManager::new();
assert!(lm.shards().is_power_of_two());Sourcepub fn with_shards(shards: usize) -> Self
pub fn with_shards(shards: usize) -> Self
Creates a lock manager with an explicit shard count.
shards is rounded up to the next power of two (and a request of 0 is
treated as 1), which lets the shard lookup use a shift instead of a
remainder. More shards reduce contention but cost a mutex and two small
maps each; fewer shards save memory at the cost of more collisions.
§Examples
use lock_db::LockManager;
// Rounded up to the next power of two.
assert_eq!(LockManager::with_shards(5).shards(), 8);
assert_eq!(LockManager::with_shards(0).shards(), 1);Sourcepub fn shards(&self) -> usize
pub fn shards(&self) -> usize
Returns the number of shards in the table.
Always a power of two.
Sourcepub fn try_acquire(
&self,
txn: TxnId,
res: ResourceId,
mode: LockMode,
) -> Result<(), LockError>
pub fn try_acquire( &self, txn: TxnId, res: ResourceId, mode: LockMode, ) -> Result<(), LockError>
Tries to acquire mode on res for txn without blocking.
The request is granted immediately and Ok(()) is returned when:
txnalready holds a lock onresthat coversmode(re-acquisition is idempotent, and asking for a weaker mode than you hold is a no-op);txnalready holdsresin some mode and the join of that mode withmodeis compatible with every other holder (an in-place upgrade — for example shared to exclusive when sole holder, or shared plus intention-exclusive to SIX); ortxnholds nothing onresandmodeis compatible with every current holder.
Otherwise nothing is changed and LockError::Conflict is returned. The
caller decides whether to retry, wait, or abort; this method never blocks
the calling thread.
§Errors
Returns LockError::Conflict if the lock cannot be granted right now.
§Examples
use lock_db::{LockError, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let key = ResourceId::new(7);
let t = TxnId::new(1);
// Upgrade a shared lock to exclusive while sole holder.
lm.try_acquire(t, key, LockMode::Shared).unwrap();
lm.try_acquire(t, key, LockMode::Exclusive).unwrap();
assert_eq!(lm.mode_held(t, key), Some(LockMode::Exclusive));
// A second reader now conflicts with the upgraded exclusive lock.
let r = lm.try_acquire(TxnId::new(2), key, LockMode::Shared);
assert_eq!(r, Err(LockError::Conflict));Sourcepub fn release(&self, txn: TxnId, res: ResourceId) -> Result<(), LockError>
pub fn release(&self, txn: TxnId, res: ResourceId) -> Result<(), LockError>
Releases the lock txn holds on res.
§Errors
Returns LockError::NotHeld if txn holds no lock on res, which
usually means a double release or a bookkeeping mismatch in the caller.
§Examples
use lock_db::{LockError, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let key = ResourceId::new(3);
let t = TxnId::new(1);
lm.try_acquire(t, key, LockMode::Exclusive).unwrap();
lm.release(t, key).unwrap();
assert_eq!(lm.release(t, key), Err(LockError::NotHeld));Sourcepub fn release_all(&self, txn: TxnId) -> usize
pub fn release_all(&self, txn: TxnId) -> usize
Releases every lock held by txn across the whole table — both point
locks and range locks.
This is the call a transaction layer makes at commit or abort to drop a transaction’s entire lock set at once. It returns the number of locks released, and is proportional to that number rather than to the size of the table.
§Examples
use lock_db::{KeyRange, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let t = TxnId::new(1);
for id in 0..5 {
lm.try_acquire(t, ResourceId::new(id), LockMode::Exclusive).unwrap();
}
lm.try_acquire_range(t, ResourceId::new(99), KeyRange::point(1), LockMode::Shared).unwrap();
assert_eq!(lm.release_all(t), 6); // 5 point locks + 1 range lock
assert_eq!(lm.release_all(t), 0); // idempotent once emptySourcepub fn request(
&self,
txn: TxnId,
res: ResourceId,
mode: LockMode,
) -> Acquisition
pub fn request( &self, txn: TxnId, res: ResourceId, mode: LockMode, ) -> Acquisition
Acquires mode on res for txn, registering a wait and detecting
deadlock if it cannot be granted.
This is the deadlock-aware counterpart to
try_acquire. The three outcomes are:
Acquisition::Granted— the lock was granted; proceed.Acquisition::Waiting— the lock is held incompatibly andtxnis now recorded in the wait-for graph. The caller should suspend the transaction and callrequestagain later (for example after a release) to retry. No deadlock was found.Acquisition::Deadlock— granting the wait would close a cycle. The caller must abort theDeadlock::victimwithrelease_all. The victim may betxnor another transaction in the cycle.
Detection is exact: the wait-for graph is rebuilt from the current lock
table on every call, so a wait left over from a lock that has since been
released contributes no edge, and a transaction is never reported as
deadlocked unless it genuinely is. The victim is chosen by the
VictimPolicy::Youngest policy; callers wanting a different policy can
apply WaitForGraph::pick_victim to Deadlock::cycle themselves.
Only transactions that wait through request appear in the graph; a
transaction that spins on try_acquire is invisible to deadlock
detection. Range locks (try_acquire_range)
are likewise not tracked here.
request serializes on a single wait-registry mutex, unlike the sharded
try_acquire; it is the path to use when you need deadlock detection.
§Examples
use lock_db::{Acquisition, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let (a, b) = (ResourceId::new(1), ResourceId::new(2));
let (t1, t2) = (TxnId::new(1), TxnId::new(2));
// T1 holds A, T2 holds B.
assert_eq!(lm.request(t1, a, LockMode::Exclusive), Acquisition::Granted);
assert_eq!(lm.request(t2, b, LockMode::Exclusive), Acquisition::Granted);
// T1 waits for B (held by T2): no cycle yet.
assert_eq!(lm.request(t1, b, LockMode::Exclusive), Acquisition::Waiting);
// T2 now waits for A (held by T1): that closes the cycle.
match lm.request(t2, a, LockMode::Exclusive) {
Acquisition::Deadlock(d) => {
assert_eq!(d.victim, TxnId::new(2)); // youngest in the cycle
lm.release_all(d.victim); // abort to break the deadlock
}
other => panic!("expected a deadlock, got {other:?}"),
}Sourcepub fn cancel_wait(&self, txn: TxnId)
pub fn cancel_wait(&self, txn: TxnId)
Removes any pending wait for txn from the wait-for graph.
Call this when a transaction that previously got Acquisition::Waiting
stops waiting without acquiring the lock (for example it timed out or was
aborted for another reason). release_all already
clears the wait, so this is only needed when releasing nothing.
§Examples
use lock_db::{Acquisition, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let res = ResourceId::new(1);
lm.request(TxnId::new(1), res, LockMode::Exclusive);
// T2 waits, then gives up.
assert_eq!(lm.request(TxnId::new(2), res, LockMode::Exclusive), Acquisition::Waiting);
lm.cancel_wait(TxnId::new(2));
assert_eq!(lm.waiting_count(), 0);Sourcepub fn find_deadlock(&self) -> Option<Deadlock>
pub fn find_deadlock(&self) -> Option<Deadlock>
Scans the current wait set for a deadlock, returning one if found.
This is the periodic-detection counterpart to the at-wait detection in
request: a background task can call it on an interval
instead of (or in addition to) acting on request’s result. It rebuilds
the wait-for graph from the current lock table, so it reports only
genuine deadlocks. Returns None when no cycle exists.
§Examples
use lock_db::{Acquisition, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let (a, b) = (ResourceId::new(1), ResourceId::new(2));
lm.request(TxnId::new(1), a, LockMode::Exclusive);
lm.request(TxnId::new(2), b, LockMode::Exclusive);
lm.request(TxnId::new(1), b, LockMode::Exclusive); // T1 waits for T2
assert!(lm.find_deadlock().is_none());
lm.request(TxnId::new(2), a, LockMode::Exclusive); // T2 waits for T1: cycle
assert!(lm.find_deadlock().is_some());Sourcepub fn waiting_count(&self) -> usize
pub fn waiting_count(&self) -> usize
Returns the number of transactions currently registered as waiting.
Mostly useful for diagnostics and tests.
Sourcepub fn holder_count(&self, res: ResourceId) -> usize
pub fn holder_count(&self, res: ResourceId) -> usize
Returns the number of transactions currently holding res.
Mostly useful for diagnostics and tests; in steady state this is 0,
1 for an exclusive lock, or the reader count for a shared lock.
§Examples
use lock_db::{LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let key = ResourceId::new(1);
assert_eq!(lm.holder_count(key), 0);
lm.try_acquire(TxnId::new(1), key, LockMode::Shared).unwrap();
assert_eq!(lm.holder_count(key), 1);Sourcepub fn mode_held(&self, txn: TxnId, res: ResourceId) -> Option<LockMode>
pub fn mode_held(&self, txn: TxnId, res: ResourceId) -> Option<LockMode>
Returns the mode in which txn holds res, or None if it holds no
lock on it.
§Examples
use lock_db::{LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let key = ResourceId::new(1);
let t = TxnId::new(1);
assert_eq!(lm.mode_held(t, key), None);
lm.try_acquire(t, key, LockMode::Shared).unwrap();
assert_eq!(lm.mode_held(t, key), Some(LockMode::Shared));Sourcepub fn try_acquire_range(
&self,
txn: TxnId,
space: ResourceId,
range: KeyRange,
mode: LockMode,
) -> Result<(), LockError>
pub fn try_acquire_range( &self, txn: TxnId, space: ResourceId, range: KeyRange, mode: LockMode, ) -> Result<(), LockError>
Tries to acquire mode over the key range range in key space space,
for txn, without blocking.
A range lock protects a contiguous span of keys — use it to stop another
transaction from inserting into, or writing within, a range you have
read (phantom and predicate protection). space identifies the key space
the range lives in, typically an index; ranges in different spaces never
conflict.
The request is granted unless some other transaction already holds an
overlapping range in space in an
incompatible mode. The same transaction may
hold several ranges in a space, including overlapping ones; range locks
are not merged or upgraded.
§Errors
Returns LockError::Conflict if an overlapping, incompatible range is
held by another transaction.
§Examples
use lock_db::{KeyRange, LockError, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let index = ResourceId::new(1);
// A read lock over [100, 200].
lm.try_acquire_range(TxnId::new(1), index, KeyRange::new(100, 200).unwrap(), LockMode::Shared).unwrap();
// Another reader may share the overlapping range...
lm.try_acquire_range(TxnId::new(2), index, KeyRange::new(150, 250).unwrap(), LockMode::Shared).unwrap();
// ...but a writer inside it conflicts.
assert_eq!(
lm.try_acquire_range(TxnId::new(3), index, KeyRange::point(150), LockMode::Exclusive),
Err(LockError::Conflict),
);Sourcepub fn release_range(
&self,
txn: TxnId,
space: ResourceId,
range: KeyRange,
) -> Result<(), LockError>
pub fn release_range( &self, txn: TxnId, space: ResourceId, range: KeyRange, ) -> Result<(), LockError>
Releases a range lock txn holds over range in space.
Matches on the transaction and the exact range. If the transaction holds several locks on the identical range (in different modes), one is released per call.
§Errors
Returns LockError::NotHeld if txn holds no lock on that exact range
in space.
§Examples
use lock_db::{KeyRange, LockError, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let index = ResourceId::new(1);
let r = KeyRange::new(1, 10).unwrap();
let t = TxnId::new(1);
lm.try_acquire_range(t, index, r, LockMode::Exclusive).unwrap();
lm.release_range(t, index, r).unwrap();
assert_eq!(lm.release_range(t, index, r), Err(LockError::NotHeld));Sourcepub fn range_count(&self, space: ResourceId) -> usize
pub fn range_count(&self, space: ResourceId) -> usize
Returns the number of range locks currently held in space.
Counts every holder, across all transactions and modes. Mostly useful for diagnostics and tests.
§Examples
use lock_db::{KeyRange, LockManager, LockMode, ResourceId, TxnId};
let lm = LockManager::new();
let index = ResourceId::new(1);
assert_eq!(lm.range_count(index), 0);
lm.try_acquire_range(TxnId::new(1), index, KeyRange::point(1), LockMode::Shared).unwrap();
assert_eq!(lm.range_count(index), 1);