···11+# lock-ordering
22+33+Compile-time deadlock prevention for Rust. Declare a partial order over your
44+locks and the type system refuses to compile any acquisition that violates it.
55+66+## Motivation
77+88+Deadlock is one of the few classes of bug Rust does not catch. Two threads each
99+hold a lock the other needs, neither can make progress, and the program hangs
1010+in production at 3am.
1111+1212+The standard fix is to pick a global acquisition order and enforce it by code
1313+review — fragile, easy to regress, and invisible at the call site. This crate
1414+turns that convention into a type-level invariant: every lock is tagged with a
1515+*level*, the levels form a DAG, and acquiring a lock consumes a proof token
1616+that records your current position in the DAG. Acquiring out of order is a type
1717+error, not a runtime hang.
1818+1919+A complementary runtime mode handles the case the type system cannot — dynamic
2020+collections like `HashMap<String, Mutex<T>>` where the acquisition order is
2121+known only at runtime. There, ascending keys are checked on each acquisition.
2222+2323+## Features
2424+2525+### Compile-time lock levels
2626+2727+Declare the lock hierarchy as a DAG. `A -> B` means: while holding `A`, you may
2828+acquire `B`. Both `A` and `B` can also be the first lock acquired.
2929+3030+```rust
3131+use lock_ordering::{define_lock_levels, LockedAt, LockLevel, MutualExclusion};
3232+use lock_ordering::lock::MutexLockLevel;
3333+use std::sync::Mutex;
3434+3535+define_lock_levels! {
3636+ ConfigCache -> MetricsCounter,
3737+}
3838+3939+impl LockLevel for ConfigCache { type Method = MutualExclusion; }
4040+impl MutexLockLevel for ConfigCache { type Mutex = Mutex<String>; }
4141+4242+impl LockLevel for MetricsCounter { type Method = MutualExclusion; }
4343+impl MutexLockLevel for MetricsCounter { type Mutex = Mutex<u64>; }
4444+4545+let config = Mutex::new(String::from("v1"));
4646+let counter = Mutex::new(0u64);
4747+4848+// SAFETY: no other LockedAt live on this thread.
4949+let locked = unsafe { LockedAt::new() };
5050+5151+let guard_cfg = locked.with_lock::<ConfigCache>(&config).unwrap();
5252+let locked_cfg = LockedAt::from_guard(&guard_cfg);
5353+let mut guard_ctr = locked_cfg.with_lock::<MetricsCounter>(&counter).unwrap();
5454+*guard_ctr += 1;
5555+5656+let _ = guard_ctr.release();
5757+let _ = guard_cfg.release();
5858+```
5959+6060+Acquiring `MetricsCounter` *before* `ConfigCache` is rejected at compile time —
6161+the required `LockBefore` bound is not satisfied.
6262+6363+### Strict ordering with `=>`
6464+6565+`A -> B` makes `B` acquirable both standalone and after `A`. Use `=>` when a
6666+level must only ever be acquired underneath a specific parent:
6767+6868+```rust
6969+define_lock_levels! {
7070+ Session -> Transaction, // both are roots
7171+ Transaction => Row, // Row REQUIRES Transaction — never a root
7272+}
7373+```
7474+7575+Acquiring `Row` as the first lock in a thread is a compile error.
7676+7777+### Cycle detection in the macro
7878+7979+```rust
8080+define_lock_levels! {
8181+ A -> B,
8282+ B -> A,
8383+}
8484+// error: cycle detected: A -> B -> A
8585+```
8686+8787+### RwLock support
8888+8989+```rust
9090+use lock_ordering::{LockLevel, ReadWrite};
9191+use lock_ordering::lock::RwLockLevel;
9292+9393+impl LockLevel for CacheIndex { type Method = ReadWrite; }
9494+impl RwLockLevel for CacheIndex { type RwLock = std::sync::RwLock<Vec<u32>>; }
9595+9696+let read_guard = locked.with_read_lock::<CacheIndex>(&rwlock).unwrap();
9797+let write_guard = locked.with_write_lock::<CacheIndex>(&rwlock).unwrap();
9898+```
9999+100100+### Async (tokio)
101101+102102+Enable the `async` feature. `wait_for_lock` / `wait_for_read` / `wait_for_write`
103103+mirror their sync counterparts and yield instead of blocking.
104104+105105+```rust
106106+let guard_a = locked.wait_for_lock::<AsyncLockA>(&mutex_a).await;
107107+let locked_a = LockedAt::from_guard(&guard_a);
108108+let guard_b = locked_a.wait_for_lock::<AsyncLockB>(&mutex_b).await;
109109+```
110110+111111+### Runtime ordered acquisition
112112+113113+For `HashMap<K, Mutex<V>>` and similar collections, the levels are all the
114114+same — the order has to come from the keys at runtime. `OrderedLock` pairs a
115115+lock with an ordering key, and `OrderedAt` enforces strictly-ascending keys at
116116+each acquisition:
117117+118118+```rust
119119+use lock_ordering::ordered::{OrderedAt, OrderedLock};
120120+121121+let apple = OrderedLock { key: "apple", lock: Mutex::new(1u32) };
122122+let banana = OrderedLock { key: "banana", lock: Mutex::new(2u32) };
123123+let cherry = OrderedLock { key: "cherry", lock: Mutex::new(3u32) };
124124+125125+let locked = unsafe { LockedAt::new() };
126126+let g_a = locked.with_ordered_lock::<MapLevel, _>(&apple).unwrap();
127127+128128+let ord_a = OrderedAt::from_guard(&g_a);
129129+let g_b = ord_a.with_lock::<MapLevel>(&banana).unwrap();
130130+131131+let ord_b = OrderedAt::from_guard(&g_b);
132132+let g_c = ord_b.with_lock::<MapLevel>(&cherry).unwrap();
133133+// Acquiring a key <= the previous one panics.
134134+```
135135+136136+Opt in to same-level chaining with `impl LockAfter<MapLevel> for MapLevel {}`,
137137+or leave it out to disallow it. `OrderedAt::into_locked_at()` exits ordered
138138+mode and recovers the compile-time token.
139139+140140+### `mem::forget` soundness
141141+142142+`LockedAt::from_guard(&guard)` borrows the guard for the lifetime of the
143143+derived token, so the borrow checker rejects `mem::forget(guard)` while a
144144+child token is live. This closes the soundness gap described in
145145+[akonradi/lock-ordering#2](https://github.com/akonradi/lock-ordering/issues/2).
146146+147147+## Acknowledgements
148148+149149+The core design — typed lock levels, `LockedAt`, and the `LockAfter` /
150150+`LockBefore` relation — is adapted from
151151+[`akonradi/lock-ordering`](https://github.com/akonradi/lock-ordering) by Alex
152152+Bakon (MIT). This crate diverges in three ways:
153153+154154+- `LockedAt::new` is `unsafe`, making the "one token per thread" invariant an
155155+ explicit obligation on the caller rather than an implicit assumption.
156156+- `with_lock` consumes the token by value and a new one is recovered through
157157+ `from_guard`, which borrows the guard — preventing the `mem::forget`
158158+ unsoundness in the original. The borrow-the-guard approach is inspired by
159159+ the discussion in
160160+ [akonradi/lock-ordering#2](https://github.com/akonradi/lock-ordering/issues/2).
161161+- A `define_lock_levels!` proc-macro generates the full transitive `LockAfter`
162162+ closure and runs cycle detection, replacing the manual
163163+ `impl_transitive_lock_order!` declarative macro.
164164+165165+The runtime ordered acquisition system (`OrderedLock` / `OrderedAt`) is new.
+1-2
lock-ordering/src/lockedat.rs
···88//! borrows the guard — preventing `mem::forget` of the guard while the derived
99//! token is live.
1010//!
1111-//! See [`LockedAt`] and [`LockGuard`] for the full API, and `.claude/plans/`
1212-//! for the design rationale.
1111+//! See [`LockedAt`] and [`LockGuard`] for the full API.
13121413use core::marker::PhantomData;
1514use core::ops::{Deref, DerefMut};