Skip to main content

distributed/lock/
async_in_memory.rs

1use std::collections::{HashMap, VecDeque};
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::{Arc, Mutex};
5use std::task::{Context, Poll, Waker};
6
7use super::{AsyncLock, AsyncLockManager, LockError};
8
9#[derive(Default)]
10struct AsyncLockState {
11    locked: bool,
12    waiters: VecDeque<Waker>,
13}
14
15/// In-memory [`AsyncLock`] backed by a `Mutex<{ locked, waiters }>`.
16///
17/// The std `Mutex` is held only for the brief state check/update — never across
18/// an `.await` — so it never blocks the executor. Acquisition returns a future
19/// that, while the lock is held, registers the task's waker and yields
20/// `Pending`; `unlock` wakes all registered waiters so they re-contend (one
21/// wins, the rest re-register). Runtime-agnostic: no dependency on any async
22/// runtime, matching the rest of the crate's RPITIT async surface.
23pub struct InMemoryAsyncLock {
24    state: Mutex<AsyncLockState>,
25}
26
27impl InMemoryAsyncLock {
28    pub fn new() -> Self {
29        InMemoryAsyncLock {
30            state: Mutex::new(AsyncLockState::default()),
31        }
32    }
33
34    /// Synchronous core of [`try_lock`](AsyncLock::try_lock).
35    ///
36    /// In-memory acquisition is pure state mutation, so the real work lives in a
37    /// private synchronous helper and the public `AsyncLock::try_lock` runs it
38    /// inside its (lazy) future — there is no parallel sync *API*, only this
39    /// internal detail. The synchronous core also lets the regression tests
40    /// exercise acquisition from inside a `Waker`, which cannot `.await`.
41    fn try_lock_core(&self) -> Result<bool, LockError> {
42        let mut state = self
43            .state
44            .lock()
45            .map_err(|err| LockError::Poisoned(err.to_string()))?;
46        if state.locked {
47            Ok(false)
48        } else {
49            state.locked = true;
50            Ok(true)
51        }
52    }
53
54    /// Synchronous core of [`unlock`](AsyncLock::unlock).
55    ///
56    /// Drains waiters UNDER the guard (keeping register/drain mutually exclusive
57    /// so no wakeup is lost), then releases the guard BEFORE waking.
58    /// `Waker::wake` runs arbitrary executor code: doing it under the std
59    /// `Mutex` would let a panicking waker poison (permanently brick) the lock,
60    /// and a waker that synchronously re-polls would deadlock on the
61    /// non-reentrant guard. Waking outside the critical section avoids both.
62    ///
63    /// `pub(crate)` so the SQLx locks' cancellation-safe gate guard can release
64    /// the in-process gate synchronously from `Drop` (which cannot `.await`).
65    pub(crate) fn unlock_core(&self) -> Result<(), LockError> {
66        let woken = {
67            let mut state = self
68                .state
69                .lock()
70                .map_err(|err| LockError::Poisoned(err.to_string()))?;
71            if state.locked {
72                state.locked = false;
73                std::mem::take(&mut state.waiters)
74            } else {
75                VecDeque::new()
76            }
77        };
78        // They re-contend and one wins, the rest re-register on their next poll.
79        for waker in woken {
80            waker.wake();
81        }
82        Ok(())
83    }
84}
85
86impl Default for InMemoryAsyncLock {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Future returned by [`InMemoryAsyncLock::lock`].
93///
94/// Borrows the lock for its lifetime; resolves once the lock is acquired.
95pub struct InMemoryAsyncLockFuture<'a> {
96    lock: &'a InMemoryAsyncLock,
97}
98
99impl Future for InMemoryAsyncLockFuture<'_> {
100    type Output = Result<(), LockError>;
101
102    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
103        let mut state = match self.lock.state.lock() {
104            Ok(state) => state,
105            Err(err) => return Poll::Ready(Err(LockError::Poisoned(err.to_string()))),
106        };
107        if !state.locked {
108            state.locked = true;
109            Poll::Ready(Ok(()))
110        } else {
111            // Register (or refresh) this task's waker so `unlock` can wake it.
112            // Dedupe by `will_wake` so repeated polls without an intervening
113            // unlock do not accumulate duplicate wakers.
114            if !state
115                .waiters
116                .iter()
117                .any(|waker| waker.will_wake(cx.waker()))
118            {
119                state.waiters.push_back(cx.waker().clone());
120            }
121            Poll::Pending
122        }
123    }
124}
125
126impl AsyncLock for InMemoryAsyncLock {
127    fn lock(&self) -> impl Future<Output = Result<(), LockError>> + Send + '_ {
128        InMemoryAsyncLockFuture { lock: self }
129    }
130
131    // Lazy: the side effect runs when the future is polled, not at call time, so
132    // a future that is dropped without being awaited is a no-op — matching the
133    // I/O-backed locks (whose `async fn` bodies also only run on poll). The body
134    // has no `.await`, so the returned future is trivially `Send`.
135    async fn try_lock(&self) -> Result<bool, LockError> {
136        self.try_lock_core()
137    }
138
139    async fn unlock(&self) -> Result<(), LockError> {
140        self.unlock_core()
141    }
142}
143
144/// In-memory [`AsyncLockManager`] backed by a `HashMap<String, Arc<InMemoryAsyncLock>>`.
145///
146/// Lazily creates one [`InMemoryAsyncLock`] per unique key and returns the same
147/// `Arc` for repeated lookups — the async counterpart to
148/// [`InMemoryLockManager`](super::InMemoryLockManager).
149pub struct InMemoryAsyncLockManager {
150    locks: Mutex<HashMap<String, Arc<InMemoryAsyncLock>>>,
151}
152
153impl InMemoryAsyncLockManager {
154    pub fn new() -> Self {
155        InMemoryAsyncLockManager {
156            locks: Mutex::new(HashMap::new()),
157        }
158    }
159}
160
161impl Default for InMemoryAsyncLockManager {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl AsyncLockManager for InMemoryAsyncLockManager {
168    type Lock = InMemoryAsyncLock;
169
170    fn get_lock(&self, id: &str) -> Result<Arc<InMemoryAsyncLock>, LockError> {
171        let mut locks = self
172            .locks
173            .lock()
174            .map_err(|_| LockError::Poisoned("async lock manager map poisoned".into()))?;
175        Ok(locks
176            .entry(id.to_string())
177            .or_insert_with(|| Arc::new(InMemoryAsyncLock::new()))
178            .clone())
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::sync::atomic::{AtomicUsize, Ordering};
186    use std::sync::mpsc;
187    use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
188    use std::time::Duration;
189
190    /// A `Waker` whose `wake()` re-enters the given lock via `try_lock_core()`,
191    /// modeling an inline-polling executor. (`wake` is synchronous, so it calls
192    /// the synchronous core rather than the `async` trait method.) The data
193    /// pointer is an `Arc<InMemoryAsyncLock>`.
194    fn reentrant_waker(lock: Arc<InMemoryAsyncLock>) -> Waker {
195        unsafe fn clone(data: *const ()) -> RawWaker {
196            let arc = unsafe { Arc::from_raw(data as *const InMemoryAsyncLock) };
197            let cloned = Arc::clone(&arc);
198            std::mem::forget(arc);
199            RawWaker::new(Arc::into_raw(cloned) as *const (), &REENTRANT_VTABLE)
200        }
201        unsafe fn wake(data: *const ()) {
202            let arc = unsafe { Arc::from_raw(data as *const InMemoryAsyncLock) };
203            let _ = arc.try_lock_core(); // re-enter from inside wake(): must not deadlock
204        }
205        unsafe fn wake_by_ref(data: *const ()) {
206            let arc = unsafe { Arc::from_raw(data as *const InMemoryAsyncLock) };
207            let _ = arc.try_lock_core();
208            std::mem::forget(arc);
209        }
210        unsafe fn drop_fn(data: *const ()) {
211            drop(unsafe { Arc::from_raw(data as *const InMemoryAsyncLock) });
212        }
213        static REENTRANT_VTABLE: RawWakerVTable =
214            RawWakerVTable::new(clone, wake, wake_by_ref, drop_fn);
215        let raw = RawWaker::new(Arc::into_raw(lock) as *const (), &REENTRANT_VTABLE);
216        unsafe { Waker::from_raw(raw) }
217    }
218
219    /// A `Waker` whose `wake()` panics, modeling a misbehaving executor.
220    fn panicking_waker() -> Waker {
221        unsafe fn clone(_: *const ()) -> RawWaker {
222            RawWaker::new(std::ptr::null(), &PANIC_VTABLE)
223        }
224        unsafe fn wake(_: *const ()) {
225            panic!("waker panicked in wake()");
226        }
227        unsafe fn wake_by_ref(_: *const ()) {
228            panic!("waker panicked in wake_by_ref()");
229        }
230        unsafe fn drop_fn(_: *const ()) {}
231        static PANIC_VTABLE: RawWakerVTable =
232            RawWakerVTable::new(clone, wake, wake_by_ref, drop_fn);
233        unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &PANIC_VTABLE)) }
234    }
235
236    /// Park `waker` on the held `lock` by polling one acquire future to `Pending`.
237    fn park_waker(lock: &InMemoryAsyncLock, waker: &Waker) {
238        let mut cx = Context::from_waker(waker);
239        let mut fut = std::pin::pin!(lock.lock());
240        assert!(matches!(fut.as_mut().poll(&mut cx), Poll::Pending));
241    }
242
243    #[tokio::test]
244    async fn try_lock_reflects_state() {
245        let lock = InMemoryAsyncLock::new();
246        assert!(lock.try_lock().await.unwrap()); // free → acquired
247        assert!(!lock.try_lock().await.unwrap()); // held → fails
248        lock.unlock().await.unwrap();
249        assert!(lock.try_lock().await.unwrap()); // released → acquired again
250    }
251
252    #[tokio::test]
253    async fn lock_resolves_immediately_when_free() {
254        let lock = InMemoryAsyncLock::new();
255        lock.lock().await.unwrap();
256        assert!(!lock.try_lock().await.unwrap()); // now held
257        lock.unlock().await.unwrap();
258        assert!(lock.try_lock().await.unwrap());
259    }
260
261    #[tokio::test]
262    async fn second_acquire_waits_until_unlock() {
263        let lock = Arc::new(InMemoryAsyncLock::new());
264        lock.lock().await.unwrap();
265
266        let order = Arc::new(AtomicUsize::new(0));
267        let waiter_lock = Arc::clone(&lock);
268        let waiter_order = Arc::clone(&order);
269        let waiter = tokio::spawn(async move {
270            waiter_lock.lock().await.unwrap();
271            // Records the order in which it acquired (must be after unlock below).
272            waiter_order.fetch_add(1, Ordering::SeqCst)
273        });
274
275        // Give the waiter time to park on the held lock.
276        tokio::time::sleep(Duration::from_millis(20)).await;
277        assert_eq!(
278            order.load(Ordering::SeqCst),
279            0,
280            "waiter must still be parked"
281        );
282
283        lock.unlock().await.unwrap();
284        let acquired_at = waiter.await.unwrap();
285        assert_eq!(acquired_at, 0, "waiter acquired exactly once after unlock");
286        assert!(!lock.try_lock().await.unwrap(), "waiter holds the lock");
287    }
288
289    #[test]
290    fn manager_returns_same_arc_per_key() {
291        let manager = InMemoryAsyncLockManager::new();
292        let a1 = manager.get_lock("agg-1").unwrap();
293        let a2 = manager.get_lock("agg-1").unwrap();
294        let b = manager.get_lock("agg-2").unwrap();
295        assert!(Arc::ptr_eq(&a1, &a2));
296        assert!(!Arc::ptr_eq(&a1, &b));
297    }
298
299    #[tokio::test]
300    async fn distinct_keys_do_not_contend() {
301        let manager = InMemoryAsyncLockManager::new();
302        let a = manager.get_lock("agg-1").unwrap();
303        let b = manager.get_lock("agg-2").unwrap();
304        a.lock().await.unwrap();
305        // Different key acquires without waiting on `a`.
306        b.lock().await.unwrap();
307        a.unlock().await.unwrap();
308        b.unlock().await.unwrap();
309    }
310
311    // Regression: `unlock` must wake waiters OUTSIDE the held guard, so a waker
312    // that synchronously re-polls the lock cannot deadlock on the non-reentrant
313    // std `Mutex`. Without the fix this hangs; the watchdog turns that into a
314    // failure instead of wedging the suite.
315    #[test]
316    fn unlock_does_not_deadlock_with_reentrant_waker() {
317        let lock = Arc::new(InMemoryAsyncLock::new());
318        assert!(lock.try_lock_core().unwrap()); // hold the lock
319        park_waker(&lock, &reentrant_waker(Arc::clone(&lock)));
320
321        let (tx, rx) = mpsc::channel();
322        let unlock_lock = Arc::clone(&lock);
323        std::thread::spawn(move || {
324            let _ = tx.send(unlock_lock.unlock_core());
325        });
326        let result = rx
327            .recv_timeout(Duration::from_secs(2))
328            .expect("unlock deadlocked while waking a re-entrant waker");
329        result.expect("unlock should succeed");
330    }
331
332    // Regression: a panicking waker must not poison the lock's mutex, because
333    // `unlock` releases the guard before waking. After the panic the lock is
334    // still usable (and was released).
335    #[test]
336    fn unlock_does_not_poison_when_a_waker_panics() {
337        let lock = Arc::new(InMemoryAsyncLock::new());
338        assert!(lock.try_lock_core().unwrap()); // hold the lock
339        park_waker(&lock, &panicking_waker());
340
341        let unlock_lock = Arc::clone(&lock);
342        let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
343            let _ = unlock_lock.unlock_core();
344        }))
345        .is_err();
346        assert!(panicked, "the panicking waker should unwind out of unlock");
347
348        // Not poisoned: the guard was dropped before the panicking wake ran, and
349        // the lock was released, so it can be acquired again.
350        assert!(
351            lock.try_lock_core().unwrap(),
352            "lock must remain usable after a waker panic"
353        );
354    }
355}