Skip to main content

fyrox_core/
safelock.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! This is an extension for Mutex-like objects that creates a `safe_lock` method
22//! to replace `lock` and put a time limit on locking, preventing a game from
23//! permanently freezing due to a deadlock.
24
25use parking_lot::{Mutex, MutexGuard};
26#[allow(unused_imports)]
27use std::sync::TryLockError;
28use std::time::Duration;
29
30#[allow(dead_code)]
31const PANIC_MESSAGE: &str = "lock timeout";
32
33/// Trait for lockable objects that can panic if they take too long
34/// to lock. Panicking is preferable over freezing.
35pub trait SafeLock {
36    const TIMEOUT: Duration = Duration::from_secs(10);
37    type Output<'a>
38    where
39        Self: 'a;
40    /// Attempt to lock the object, with a limit on how long locking may block for,
41    /// panicking if the time limit is exceeded. It panics instead of freezing.
42    fn safe_lock(&self) -> Self::Output<'_>;
43}
44
45impl<T: ?Sized> SafeLock for Mutex<T> {
46    type Output<'a>
47        = MutexGuard<'a, T>
48    where
49        T: 'a;
50    #[cfg(target_arch = "wasm32")]
51    fn safe_lock(&self) -> Self::Output<'_> {
52        self.lock()
53    }
54    #[cfg(not(target_arch = "wasm32"))]
55    fn safe_lock(&self) -> Self::Output<'_> {
56        self.try_lock_for(Self::TIMEOUT).expect(PANIC_MESSAGE)
57    }
58}
59
60impl<T: ?Sized> SafeLock for std::sync::Mutex<T> {
61    type Output<'a>
62        = std::sync::LockResult<std::sync::MutexGuard<'a, T>>
63    where
64        T: 'a;
65
66    #[cfg(target_arch = "wasm32")]
67    fn safe_lock(&self) -> Self::Output<'_> {
68        self.lock()
69    }
70    #[cfg(not(target_arch = "wasm32"))]
71    fn safe_lock(&self) -> Self::Output<'_> {
72        let start = std::time::Instant::now();
73        loop {
74            match self.try_lock() {
75                Ok(guard) => return Ok(guard),
76                Err(TryLockError::WouldBlock) => (),
77                Err(TryLockError::Poisoned(err)) => return Err(err),
78            }
79            std::thread::yield_now();
80            if start.elapsed() > Self::TIMEOUT {
81                std::panic::panic_any(PANIC_MESSAGE);
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod test {
89    use super::*;
90    use std::{
91        any::Any,
92        panic::{catch_unwind, AssertUnwindSafe},
93    };
94    fn panic_to_string(message: Box<dyn Any>) -> Option<String> {
95        match message.downcast_ref::<&str>() {
96            Some(&str) => Some(str.into()),
97            None => message.downcast::<String>().ok().map(|s| *s),
98        }
99    }
100    #[test]
101    fn successful_lock_parking_lot() {
102        let mutex = Mutex::new(());
103        drop(mutex.safe_lock());
104    }
105    #[test]
106    fn successful_lock_std() {
107        let mutex = std::sync::Mutex::new(());
108        drop(mutex.safe_lock());
109    }
110    #[test]
111    fn failed_lock_parking_lot() {
112        let mutex = Mutex::new(());
113        let _guard = mutex.safe_lock();
114        let panic_message = catch_unwind(AssertUnwindSafe(|| mutex.safe_lock()))
115            .expect_err("safe_lock did not panic");
116        let Some(message) = panic_to_string(panic_message) else {
117            panic!("safe_lock panicked with wrong type");
118        };
119        assert_eq!(message, PANIC_MESSAGE);
120    }
121    #[test]
122    fn failed_lock_std() {
123        let mutex = std::sync::Mutex::new(());
124        let _guard = mutex.safe_lock();
125        let panic_message =
126            catch_unwind(|| mutex.safe_lock()).expect_err("safe_lock did not panic");
127        let Some(message) = panic_to_string(panic_message) else {
128            panic!("safe_lock panicked with wrong type");
129        };
130        assert_eq!(message, PANIC_MESSAGE);
131    }
132}