thread-checked-lock 0.1.1

Gracefully error when a thread attempts to acquire the same lock twice
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
use std::{
    fmt::{Display, Formatter, Result as FmtResult},
    ops::{Deref, DerefMut},
    sync::{Mutex, MutexGuard, PoisonError, TryLockError as StdTryLockError},
};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::{locked_mutexes, mutex_id};
use crate::mutex_id::MutexID;
use crate::error::{AccessResult, LockError, LockResult, TryLockError, TryLockResult};


/// A variant of [`std::sync::Mutex`] which gracefully returns an error when a thread attempts
/// to acquire a `ThreadCheckedMutex` that it already holds.
///
/// In such a situation, [`Mutex::lock`] is guaranteed to either lock or panic, while
/// [`Mutex::try_lock`] checks if *any* thread holds the lock (and cannot distinguish whether the
/// current thread holds the lock). As such, attempting to lock the same `Mutex` twice on a thread
/// is potentially a fatal error; `ThreadCheckedMutex` allows for recovery.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug)]
pub struct ThreadCheckedMutex<T: ?Sized> {
    mutex_id: MutexID,
    mutex:    Mutex<T>,
}

impl<T> ThreadCheckedMutex<T> {
    /// Creates a new mutex in an unlocked state.
    #[inline]
    #[must_use]
    pub fn new(t: T) -> Self {
        Self {
            mutex_id: mutex_id::next_id(),
            mutex:    Mutex::new(t),
        }
    }
}

impl<T: ?Sized> ThreadCheckedMutex<T> {
    /// Helper function for creating a [`ThreadCheckedMutexGuard`] from a [`MutexGuard`].
    #[inline]
    const fn new_guard<'a>(&self, guard: MutexGuard<'a, T>) -> ThreadCheckedMutexGuard<'a, T> {
        ThreadCheckedMutexGuard {
            mutex_id: self.mutex_id,
            guard,
        }
    }

    /// Helper function for mapping the type inside a [`PoisonError`] from [`MutexGuard`] to
    /// [`ThreadCheckedMutexGuard`].
    #[inline]
    fn poisoned_guard<'a>(
        &self,
        poison: PoisonError<MutexGuard<'a, T>>,
    ) -> PoisonError<ThreadCheckedMutexGuard<'a, T>> {
        PoisonError::new(self.new_guard(poison.into_inner()))
    }
}

impl<T: ?Sized> ThreadCheckedMutex<T> {
    /// Attempts to acquire this mutex, blocking the current thread while the mutex is locked in
    /// other threads.
    ///
    /// If the mutex is acquired (either completely successfully or with a poison error), a
    /// [`ThreadCheckedMutexGuard`] is returned. Only one thread at a time can hold the lock; at
    /// most one [`ThreadCheckedMutexGuard`] can exist at a time (across any thread); and the mutex
    /// is unlocked when the returned guard is dropped.
    ///
    /// # Errors
    /// If the mutex was already held by the current thread when this call was made, then a
    /// [`LockedByCurrentThread`] error is returned.
    ///
    /// If another user of this mutex panicked while holding the mutex, then this call will still
    /// acquire the mutex but wrap the returned guard in a poison error. See the
    /// [`HandlePoisonResult`] trait for methods to ignore poison errors and treat them as
    /// successful, or to panic if a poison error was returned.
    ///
    /// [`HandlePoisonResult`]: crate::HandlePoisonResult
    /// [`LockedByCurrentThread`]: LockError::LockedByCurrentThread
    pub fn lock(&self) -> LockResult<ThreadCheckedMutexGuard<'_, T>> {
        if locked_mutexes::register_locked(self.mutex_id) {
            match self.mutex.lock() {
                Ok(guard)   => Ok(self.new_guard(guard)),
                Err(poison) => {
                    let poison = self.poisoned_guard(poison);
                    Err(LockError::Poisoned(poison))
                }
            }
        } else {
            Err(LockError::LockedByCurrentThread)
        }
    }

    /// Attempts to acquire this mutex without blocking.
    ///
    /// If the mutex is acquired (either completely successfully or with a poison error), a
    /// [`ThreadCheckedMutexGuard`] is returned. Only one thread at a time can hold the lock; at
    /// most one [`ThreadCheckedMutexGuard`] can exist at a time (across any thread); and the mutex
    /// is unlocked when the returned guard is dropped.
    ///
    /// # Errors
    /// If the mutex was already held by the current thread when this call was made, then a
    /// [`LockedByCurrentThread`] error is returned. If the mutex was held by a different thread,
    /// then a [`WouldBlock`] error is returned.
    ///
    /// If another user of this mutex panicked while holding the mutex, then this call will still
    /// acquire the mutex but wrap the returned guard in a poison error. See the
    /// [`HandlePoisonResult`] trait for methods to ignore poison errors and treat them as
    /// successful, or to panic if a poison error was returned.
    ///
    /// [`HandlePoisonResult`]: crate::HandlePoisonResult
    /// [`LockedByCurrentThread`]: TryLockError::LockedByCurrentThread
    /// [`WouldBlock`]: TryLockError::WouldBlock
    pub fn try_lock(&self) -> TryLockResult<ThreadCheckedMutexGuard<'_, T>> {
        if self.locked_by_current_thread() {
            return Err(TryLockError::LockedByCurrentThread);
        }

        match self.mutex.try_lock() {
            Ok(guard) => {
                #[expect(
                    clippy::let_underscore_must_use,
                    clippy::redundant_type_annotations,
                    reason = "We already checked that the current thread hasn't locked the mutex, \
                              so this always returns true.",
                )]
                let _: bool = locked_mutexes::register_locked(self.mutex_id);
                Ok(self.new_guard(guard))
            }
            Err(StdTryLockError::Poisoned(poison)) => {
                #[expect(
                    clippy::let_underscore_must_use,
                    clippy::redundant_type_annotations,
                    reason = "We already checked that the current thread hasn't locked the mutex, \
                              so this always returns true.",
                )]
                let _: bool = locked_mutexes::register_locked(self.mutex_id);
                let poison = self.poisoned_guard(poison);
                Err(TryLockError::Poisoned(poison))
            }
            Err(StdTryLockError::WouldBlock) => Err(TryLockError::WouldBlock),
        }
    }

    /// Determines whether this mutex is currently held by the current thread.
    #[inline]
    #[must_use]
    pub fn locked_by_current_thread(&self) -> bool {
        locked_mutexes::locked_by_current_thread(self.mutex_id)
    }

    /// Determines whether this mutex is currently poisoned.
    ///
    /// If another thread is active, the mutex could become poisoned or have its poison cleared
    /// at any time; as such, the return value of this function should generally not be depended on
    /// for program correctness.
    ///
    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
    #[inline]
    #[must_use]
    pub fn is_poisoned(&self) -> bool {
        self.mutex.is_poisoned()
    }

    /// Clear any poison from this mutex.
    ///
    /// When a [`ThreadCheckedMutexGuard`] is dropped in a thread which is panicking, its associated
    /// mutex becomes poisoned, and remains poisoned until this function is called (by any thread).
    ///
    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
    #[inline]
    pub fn clear_poison(&self) {
        self.mutex.clear_poison();
    }

    /// Consumes this mutex and returns the underlying data.
    ///
    /// # Errors
    /// If another user of this mutex panicked while holding the mutex, then the inner data is
    /// still returned, but wrapped in a poison error.
    ///
    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
    #[inline]
    pub fn into_inner(self) -> AccessResult<T>
    where
        T: Sized,
    {
        self.mutex.into_inner().map_err(Into::into)
    }

    /// Returns a mutable reference to the underlying data, without locking.
    ///
    /// # Errors
    /// If another user of this mutex panicked while holding the mutex, then a mutable reference is
    /// still returned, but wrapped in a poison error.
    ///
    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
    #[inline]
    pub fn get_mut(&mut self) -> AccessResult<&mut T> {
        self.mutex.get_mut().map_err(Into::into)
    }
}

impl<T: Default> Default for ThreadCheckedMutex<T> {
    #[inline]
    fn default() -> Self {
        Self::new(T::default())
    }
}

/// A RAII scoped lock for a [`ThreadCheckedMutex`], analogous to [`MutexGuard`] for [`Mutex`].
///
/// When this guard is dropped, the corresponding [`ThreadCheckedMutex`] is unlocked. The guard
/// provides access to the mutex's protected data via [`Deref`] and [`DerefMut`].
///
/// This structure can be created via the [`lock`] and [`try_lock`] methods of
/// [`ThreadCheckedMutex`].
///
/// [`lock`]: ThreadCheckedMutex::lock
/// [`try_lock`]: ThreadCheckedMutex::try_lock
#[must_use = "if unused the ThreadCheckedMutex will immediately unlock"]
#[clippy::has_significant_drop]
#[derive(Debug)]
pub struct ThreadCheckedMutexGuard<'a, T: ?Sized> {
    mutex_id: MutexID,
    guard:    MutexGuard<'a, T>,
}

impl<T: ?Sized> Drop for ThreadCheckedMutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        let was_locked = locked_mutexes::register_unlocked(self.mutex_id);

        // This assertion should not fail unless someone used unsound unsafe code.
        debug_assert!(
            was_locked,
            "a ThreadCheckedMutexGuard was dropped in a thread which it was not locked in",
        );
    }
}

impl<T: ?Sized> Deref for ThreadCheckedMutexGuard<'_, T> {
    type Target = T;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.guard
    }
}

impl<T: ?Sized> DerefMut for ThreadCheckedMutexGuard<'_, T> {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.guard
    }
}

impl<T: ?Sized + Display> Display for ThreadCheckedMutexGuard<'_, T> {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        Display::fmt(&*self.guard, f)
    }
}


#[cfg(test)]
mod tests {
    #![expect(clippy::unwrap_used, reason = "these are tests")]

    use std::{sync::mpsc, thread};
    use std::{sync::Arc, time::Duration};

    use crate::mutex_id::run_this_before_each_test_that_creates_a_mutex_id;
    use super::*;


    #[test]
    fn lock_then_is_locked() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = ThreadCheckedMutex::new(0_u8);

        assert!(!mutex.locked_by_current_thread());

        let _guard = mutex.lock().unwrap();

        assert!(mutex.locked_by_current_thread());
    }

    #[test]
    fn lock_unlock_isnt_locked() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = ThreadCheckedMutex::new(0_u8);

        let guard = mutex.lock().unwrap();

        assert!(mutex.locked_by_current_thread());

        drop(guard);

        assert!(!mutex.locked_by_current_thread());
    }

    #[test]
    fn lock_unlock_lock() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = ThreadCheckedMutex::new(0_u8);

        {
            let _guard = mutex.lock().unwrap();
        }

        assert!(!mutex.locked_by_current_thread());

        let _guard = mutex.lock().unwrap();

        assert!(mutex.locked_by_current_thread());
    }

    #[test]
    fn lock_lock_unlock_lock() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = ThreadCheckedMutex::new(0_u8);

        let guard = mutex.lock().unwrap();

        // An additional attempt to lock should fail.
        assert!(matches!(
            mutex.lock(),
            Err(LockError::LockedByCurrentThread),
        ));

        drop(guard);

        // Now it should succeed.
        let _guard = mutex.lock().unwrap();
    }

    #[test]
    fn locked_by_current_thread() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = Arc::new(ThreadCheckedMutex::new(()));
        let (sender, receiver) = mpsc::channel();

        let mutex_clone = Arc::clone(&mutex);

        thread::spawn(move || {
            let guard = mutex_clone.try_lock().unwrap();
            drop(guard);
            sender.send(()).unwrap();
        });

        // Wait to receive something.
        receiver.recv().unwrap();

        // The mutex should have been unlocked before we received anything.
        let _guard = mutex.try_lock().unwrap();

        // An additional attempt to lock should fail.
        assert!(matches!(
            mutex.try_lock(),
            Err(TryLockError::LockedByCurrentThread),
        ));
    }

    #[test]
    fn would_block() {
        run_this_before_each_test_that_creates_a_mutex_id();

        let mutex = Arc::new(ThreadCheckedMutex::new(()));
        let (locking_sender, locking_receiver) = mpsc::channel();
        let (unlocking_sender, unlocking_receiver) = mpsc::channel();

        let mutex_clone = Arc::clone(&mutex);

        thread::spawn(move || {
            let guard = mutex_clone.try_lock().unwrap();

            locking_sender.send(()).unwrap();

            // Wait to receive something.
            unlocking_receiver.recv().unwrap();

            // Block for a bit, to try to ensure that `lock` is capable of waiting.
            thread::sleep(Duration::from_millis(50));

            drop(guard);
        });

        // Wait to receive something.
        locking_receiver.recv().unwrap();

        // The mutex should have been locked before we received anything, and since we haven't
        // sent anything, it should still be locked.

        assert!(matches!(
            mutex.try_lock(),
            Err(TryLockError::WouldBlock),
        ));

        unlocking_sender.send(()).unwrap();

        // Now `lock` should work, though `try_lock` might not.
        let _guard = mutex.lock().unwrap();
    }
}