Skip to main content

shuttle_std/sync/
rwlock.rs

1use crate::sync::{ResourceSignature, ResourceType};
2use shuttle_engine::future::batch_semaphore::{BatchSemaphore, Fairness};
3use shuttle_engine::runtime::execution::ExecutionState;
4use shuttle_engine::runtime::task::{TaskId, TaskSet};
5use std::cell::RefCell;
6use std::fmt::{Debug, Display};
7use std::ops::{Deref, DerefMut};
8use std::panic::{RefUnwindSafe, UnwindSafe};
9use std::sync::{LockResult, PoisonError, TryLockError, TryLockResult};
10use tracing::trace;
11
12/// (Theoretical) max number of readers holding the same `RwLock`. Based on
13/// the `tokio` implementation.
14const MAX_READS: usize = (u32::MAX >> 3) as usize;
15
16/// A reader-writer lock, the same as [`std::sync::RwLock`].
17///
18/// Unlike [`std::sync::RwLock`], the same thread is never allowed to acquire the read side of a
19/// `RwLock` more than once. The `std` version is ambiguous about what behavior is allowed here, so
20/// we choose the most conservative one.
21pub struct RwLock<T: ?Sized> {
22    state: RefCell<RwLockState>,
23    semaphore: BatchSemaphore,
24    inner: std::sync::RwLock<T>,
25}
26
27#[derive(Debug)]
28struct RwLockState {
29    holder: RwLockHolder,
30}
31
32#[derive(PartialEq, Eq, Debug)]
33enum RwLockHolder {
34    Read(TaskSet),
35    Write(TaskId),
36    None,
37}
38
39#[derive(PartialEq, Eq, Debug, Clone, Copy)]
40enum RwLockType {
41    Read,
42    Write,
43}
44
45impl RwLockType {
46    /// Number of semaphore permits corresponding to the given lock type.
47    fn num_permits(&self) -> usize {
48        match self {
49            Self::Read => 1,
50            Self::Write => MAX_READS,
51        }
52    }
53}
54
55impl<T> RwLock<T> {
56    /// Create a new instance of an `RwLock<T>` which is unlocked.
57    #[track_caller]
58    pub const fn new(value: T) -> Self {
59        let state = RwLockState {
60            holder: RwLockHolder::None,
61        };
62
63        Self {
64            inner: std::sync::RwLock::new(value),
65            semaphore: BatchSemaphore::const_new_with_signature(
66                MAX_READS,
67                Fairness::Unfair,
68                ResourceSignature::new_const(ResourceType::RwLock),
69            ),
70            state: RefCell::new(state),
71        }
72    }
73}
74
75impl<T: ?Sized> RwLock<T> {
76    /// Locks this rwlock with shared read access, blocking the current thread until it can be
77    /// acquired.
78    pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
79        self.lock(RwLockType::Read);
80
81        match self.inner.try_read() {
82            Ok(guard) => Ok(RwLockReadGuard {
83                inner: Some(guard),
84                rwlock: self,
85                me: ExecutionState::me(),
86            }),
87            Err(TryLockError::Poisoned(err)) => Err(PoisonError::new(RwLockReadGuard {
88                inner: Some(err.into_inner()),
89                rwlock: self,
90                me: ExecutionState::me(),
91            })),
92            Err(TryLockError::WouldBlock) => panic!("rwlock state out of sync"),
93        }
94    }
95
96    /// Locks this rwlock with exclusive write access, blocking the current thread until it can
97    /// be acquired.
98    pub fn write(&self) -> LockResult<RwLockWriteGuard<'_, T>> {
99        self.lock(RwLockType::Write);
100
101        match self.inner.try_write() {
102            Ok(guard) => Ok(RwLockWriteGuard {
103                inner: Some(guard),
104                rwlock: self,
105                me: ExecutionState::me(),
106            }),
107            Err(TryLockError::Poisoned(err)) => Err(PoisonError::new(RwLockWriteGuard {
108                inner: Some(err.into_inner()),
109                rwlock: self,
110                me: ExecutionState::me(),
111            })),
112            Err(TryLockError::WouldBlock) => panic!("rwlock state out of sync"),
113        }
114    }
115
116    /// Attempts to acquire this rwlock with shared read access.
117    ///
118    /// If the access could not be granted at this time, then Err is returned. This function does
119    /// not block.
120    ///
121    /// Note that unlike [`std::sync::RwLock::try_read`], if the current thread already holds this
122    /// read lock, `try_read` will return Err.
123    pub fn try_read(&self) -> TryLockResult<RwLockReadGuard<'_, T>> {
124        if self.try_lock(RwLockType::Read) {
125            match self.inner.try_read() {
126                Ok(guard) => Ok(RwLockReadGuard {
127                    inner: Some(guard),
128                    rwlock: self,
129                    me: ExecutionState::me(),
130                }),
131                Err(TryLockError::Poisoned(err)) => Err(TryLockError::Poisoned(PoisonError::new(RwLockReadGuard {
132                    inner: Some(err.into_inner()),
133                    rwlock: self,
134                    me: ExecutionState::me(),
135                }))),
136                Err(TryLockError::WouldBlock) => panic!("rwlock state out of sync"),
137            }
138        } else {
139            Err(TryLockError::WouldBlock)
140        }
141    }
142
143    /// Attempts to acquire this rwlock with shared read access.
144    ///
145    /// If the access could not be granted at this time, then Err is returned. This function does
146    /// not block.
147    pub fn try_write(&self) -> TryLockResult<RwLockWriteGuard<'_, T>> {
148        if self.try_lock(RwLockType::Write) {
149            match self.inner.try_write() {
150                Ok(guard) => Ok(RwLockWriteGuard {
151                    inner: Some(guard),
152                    rwlock: self,
153                    me: ExecutionState::me(),
154                }),
155                Err(TryLockError::Poisoned(err)) => Err(TryLockError::Poisoned(PoisonError::new(RwLockWriteGuard {
156                    inner: Some(err.into_inner()),
157                    rwlock: self,
158                    me: ExecutionState::me(),
159                }))),
160                Err(TryLockError::WouldBlock) => panic!("rwlock state out of sync"),
161            }
162        } else {
163            Err(TryLockError::WouldBlock)
164        }
165    }
166
167    /// Returns a mutable reference to the underlying data.
168    ///
169    /// Since this call borrows the `RwLock` mutably, no actual locking needs to
170    /// take place---the mutable borrow statically guarantees no locks exist.
171    #[inline]
172    pub fn get_mut(&mut self) -> LockResult<&mut T> {
173        self.inner.get_mut()
174    }
175
176    /// Consumes this `RwLock`, returning the underlying data
177    pub fn into_inner(self) -> LockResult<T>
178    where
179        T: Sized,
180    {
181        let state = self.state.borrow();
182        assert_eq!(state.holder, RwLockHolder::None);
183
184        // Update the receiver's clock with the RwLock clock
185        self.semaphore.try_acquire(MAX_READS).unwrap();
186
187        self.inner.into_inner()
188    }
189
190    /// Acquire the lock in the provided mode, blocking this thread until it succeeds.
191    fn lock(&self, typ: RwLockType) {
192        let me = ExecutionState::me();
193
194        let mut state = self.state.borrow_mut();
195        trace!(
196            holder = ?state.holder,
197            semaphore = ?self.semaphore,
198            "acquiring {:?} lock on rwlock {:p}",
199            typ,
200            self,
201        );
202        drop(state);
203
204        // Detect deadlock due to re-entrancy.
205        state = self.state.borrow_mut();
206        assert!(
207            match &state.holder {
208                RwLockHolder::Write(writer) => *writer != me,
209                RwLockHolder::Read(readers) => !readers.contains(me),
210                RwLockHolder::None => true,
211            },
212            "deadlock! task {me:?} tried to acquire a RwLock it already holds"
213        );
214        drop(state);
215
216        // The first poll of the acquire is the scheduling point before the lock operation. The
217        // acquire fails only if the semaphore is closed, which happens only if a release ran after
218        // the execution stopped (see `BatchSemaphore::release`). In that case, the lock is taken as
219        // if it were free, and the inner `std::sync::RwLock` reports whether it is poisoned.
220        let _ = self.semaphore.acquire_blocking(typ.num_permits());
221
222        state = self.state.borrow_mut();
223        match (typ, &mut state.holder) {
224            (RwLockType::Write, RwLockHolder::None) => {
225                state.holder = RwLockHolder::Write(me);
226            }
227            (RwLockType::Read, RwLockHolder::None) => {
228                let mut readers = TaskSet::new();
229                readers.insert(me);
230                state.holder = RwLockHolder::Read(readers);
231            }
232            (RwLockType::Read, RwLockHolder::Read(readers)) => {
233                assert!(readers.insert(me));
234            }
235            _ => {
236                panic!(
237                    "resumed a waiting {:?} thread while the lock was in state {:?}",
238                    typ, state.holder
239                );
240            }
241        }
242        trace!(
243            holder = ?state.holder,
244            semaphore = ?self.semaphore,
245            "acquired {:?} lock on rwlock {:p}",
246            typ,
247            self
248        );
249        drop(state);
250    }
251
252    /// Attempt to acquire this lock in the provided mode, but without blocking. Returns `true` if
253    /// the lock was able to be acquired without blocking, or `false` otherwise.
254    fn try_lock(&self, typ: RwLockType) -> bool {
255        let me = ExecutionState::me();
256
257        let mut state = self.state.borrow_mut();
258        trace!(
259            holder = ?state.holder,
260            semaphore = ?self.semaphore,
261            "trying to acquire {:?} lock on rwlock {:p}",
262            typ,
263            self,
264        );
265        drop(state);
266
267        // Semaphore is never closed, so an error here is always `NoPermits`.
268        let mut acquired = self.semaphore.try_acquire(typ.num_permits()).is_ok();
269        if acquired {
270            state = self.state.borrow_mut();
271            match (typ, &mut state.holder) {
272                (RwLockType::Write, RwLockHolder::None) => {
273                    state.holder = RwLockHolder::Write(me);
274                }
275                (RwLockType::Read, RwLockHolder::None) => {
276                    let mut readers = TaskSet::new();
277                    readers.insert(me);
278                    state.holder = RwLockHolder::Read(readers);
279                }
280                (RwLockType::Read, RwLockHolder::Read(readers)) => {
281                    // If we already hold the read lock, `insert` returns false, which will cause this
282                    // acquisition to fail with `WouldBlock` so we can diagnose potential deadlocks.
283                    acquired = readers.insert(me);
284                }
285                _ => (),
286            };
287            drop(state);
288        }
289
290        trace!(
291            "{} {:?} lock on rwlock {:p}",
292            if acquired { "acquired" } else { "failed to acquire" },
293            typ,
294            self,
295        );
296
297        acquired
298    }
299
300    /// Clear the poisoned state from a lock.
301    #[inline]
302    pub fn clear_poison(&self) {
303        self.inner.clear_poison();
304    }
305}
306
307// Safety: RwLock is never actually passed across true threads, only across continuations. The
308// Rc<RefCell<_>> type therefore can't be preempted mid-bookkeeping-operation.
309// TODO we shouldn't need to do this, but RefCell is not Send, and anything we put within a RwLock
310// TODO needs to be Send.
311unsafe impl<T: Send + ?Sized> Send for RwLock<T> {}
312unsafe impl<T: Send + ?Sized> Sync for RwLock<T> {}
313
314// TODO this is the RefCell biting us again
315impl<T: ?Sized> UnwindSafe for RwLock<T> {}
316impl<T: ?Sized> RefUnwindSafe for RwLock<T> {}
317
318impl<T: Default> Default for RwLock<T> {
319    fn default() -> Self {
320        Self::new(Default::default())
321    }
322}
323
324impl<T: ?Sized + Debug> Debug for RwLock<T> {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        Debug::fmt(&self.inner, f)
327    }
328}
329
330/// RAII structure used to release the shared read access of a `RwLock` when dropped.
331pub struct RwLockReadGuard<'a, T: ?Sized> {
332    inner: Option<std::sync::RwLockReadGuard<'a, T>>,
333    rwlock: &'a RwLock<T>,
334    me: TaskId,
335}
336
337impl<T: ?Sized> Deref for RwLockReadGuard<'_, T> {
338    type Target = T;
339
340    fn deref(&self) -> &Self::Target {
341        self.inner.as_ref().unwrap().deref()
342    }
343}
344
345impl<T: Debug> Debug for RwLockReadGuard<'_, T> {
346    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
347        Debug::fmt(&self.inner.as_ref().unwrap(), f)
348    }
349}
350
351impl<T: Display + ?Sized> Display for RwLockReadGuard<'_, T> {
352    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
353        (**self).fmt(f)
354    }
355}
356
357impl<T: ?Sized> Drop for RwLockReadGuard<'_, T> {
358    fn drop(&mut self) {
359        // Execution teardown can unwind the task from the yield point in `release` (see
360        // `ExecutionState::tear_down`). `release` releases the permit even then, and the rest of
361        // unlocking has to happen too, so that the destructors that teardown runs later can lock the
362        // `RwLock`.
363        struct Unlock<'g, 'a, T: ?Sized>(&'g mut RwLockReadGuard<'a, T>);
364        impl<T: ?Sized> Drop for Unlock<'_, '_, T> {
365            #[inline]
366            fn drop(&mut self) {
367                let guard = &mut *self.0;
368                guard.inner = None;
369
370                let mut state = guard.rwlock.state.borrow_mut();
371                trace!(
372                    holder = ?state.holder,
373                    semaphore = ?guard.rwlock.semaphore,
374                    "releasing Read lock on rwlock {:p}",
375                    guard.rwlock
376                );
377                let RwLockHolder::Read(readers) = &mut state.holder else {
378                    panic!("exiting a reader but rwlock is in the wrong state {:?}", state.holder);
379                };
380                assert!(readers.remove(guard.me));
381                if readers.is_empty() {
382                    state.holder = RwLockHolder::None;
383                }
384                drop(state);
385            }
386        }
387        let unlock = Unlock(self);
388
389        unlock.0.rwlock.semaphore.release(RwLockType::Read.num_permits());
390    }
391}
392
393/// RAII structure used to release the exclusive write access of a `RwLock` when dropped.
394pub struct RwLockWriteGuard<'a, T: ?Sized> {
395    inner: Option<std::sync::RwLockWriteGuard<'a, T>>,
396    rwlock: &'a RwLock<T>,
397    me: TaskId,
398}
399
400impl<T: ?Sized> Deref for RwLockWriteGuard<'_, T> {
401    type Target = T;
402
403    fn deref(&self) -> &Self::Target {
404        self.inner.as_ref().unwrap().deref()
405    }
406}
407
408impl<T: ?Sized> DerefMut for RwLockWriteGuard<'_, T> {
409    fn deref_mut(&mut self) -> &mut Self::Target {
410        self.inner.as_mut().unwrap().deref_mut()
411    }
412}
413
414impl<T: Debug> Debug for RwLockWriteGuard<'_, T> {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        Debug::fmt(&self.inner.as_ref().unwrap(), f)
417    }
418}
419
420impl<T: Display + ?Sized> Display for RwLockWriteGuard<'_, T> {
421    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
422        (**self).fmt(f)
423    }
424}
425
426impl<T: ?Sized> Drop for RwLockWriteGuard<'_, T> {
427    fn drop(&mut self) {
428        // As for `RwLockReadGuard`.
429        struct Unlock<'g, 'a, T: ?Sized>(&'g mut RwLockWriteGuard<'a, T>);
430        impl<T: ?Sized> Drop for Unlock<'_, '_, T> {
431            #[inline]
432            fn drop(&mut self) {
433                let guard = &mut *self.0;
434                // Teardown unwinding the task is no panic, which mustn't poison the inner lock.
435                let clear_poison = std::thread::panicking()
436                    && !guard.rwlock.inner.is_poisoned()
437                    && ExecutionState::unwinding_for_teardown();
438                guard.inner = None;
439                if clear_poison {
440                    guard.rwlock.inner.clear_poison();
441                }
442
443                let mut state = guard.rwlock.state.borrow_mut();
444                trace!(
445                    holder = ?state.holder,
446                    semaphore = ?guard.rwlock.semaphore,
447                    "releasing Write lock on rwlock {:p}",
448                    guard.rwlock
449                );
450                assert_eq!(state.holder, RwLockHolder::Write(guard.me));
451                state.holder = RwLockHolder::None;
452                drop(state);
453            }
454        }
455        let unlock = Unlock(self);
456
457        unlock.0.rwlock.semaphore.release(RwLockType::Write.num_permits());
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn unique_resource_signature_rwlock() {
467        shuttle_schedulers::check_random(
468            || {
469                let rwlock1 = RwLock::new(0);
470                let rwlock2 = RwLock::new(0);
471                assert_ne!(rwlock1.semaphore.signature(), rwlock2.semaphore.signature());
472            },
473            1,
474        );
475    }
476}