Skip to main content

uni_common/
sync.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Synchronization utilities for safe lock acquisition.
5//!
6//! This module provides helper functions that handle lock poisoning gracefully,
7//! preventing panic cascades when a thread panics while holding a lock.
8
9use std::sync::{Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
10
11/// Error returned when a lock is poisoned.
12///
13/// A lock becomes poisoned when a thread panics while holding the lock.
14/// This error type allows callers to decide how to handle poisoned locks.
15#[derive(Debug)]
16pub struct LockPoisonedError {
17    /// Description of which lock was poisoned.
18    pub lock_name: &'static str,
19}
20
21impl std::fmt::Display for LockPoisonedError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(
24            f,
25            "Lock '{}' was poisoned by a panicked thread",
26            self.lock_name
27        )
28    }
29}
30
31impl std::error::Error for LockPoisonedError {}
32
33/// Acquires a read lock, returning an error if the lock is poisoned.
34///
35/// # Errors
36///
37/// Returns `LockPoisonedError` if another thread panicked while holding this lock.
38///
39/// # Examples
40///
41/// ```
42/// use std::sync::RwLock;
43/// use uni_common::sync::acquire_read;
44///
45/// let lock = RwLock::new(42);
46/// let guard = acquire_read(&lock, "my_data").unwrap();
47/// assert_eq!(*guard, 42);
48/// ```
49pub fn acquire_read<'a, T>(
50    lock: &'a RwLock<T>,
51    lock_name: &'static str,
52) -> Result<RwLockReadGuard<'a, T>, LockPoisonedError> {
53    lock.read()
54        .map_err(|_: PoisonError<_>| LockPoisonedError { lock_name })
55}
56
57/// Acquires a write lock, returning an error if the lock is poisoned.
58///
59/// # Errors
60///
61/// Returns `LockPoisonedError` if another thread panicked while holding this lock.
62///
63/// # Examples
64///
65/// ```
66/// use std::sync::RwLock;
67/// use uni_common::sync::acquire_write;
68///
69/// let lock = RwLock::new(42);
70/// let mut guard = acquire_write(&lock, "my_data").unwrap();
71/// *guard = 100;
72/// ```
73pub fn acquire_write<'a, T>(
74    lock: &'a RwLock<T>,
75    lock_name: &'static str,
76) -> Result<RwLockWriteGuard<'a, T>, LockPoisonedError> {
77    lock.write()
78        .map_err(|_: PoisonError<_>| LockPoisonedError { lock_name })
79}
80
81/// Acquires a mutex lock, returning an error if the lock is poisoned.
82///
83/// # Errors
84///
85/// Returns `LockPoisonedError` if another thread panicked while holding this lock.
86///
87/// # Examples
88///
89/// ```
90/// use std::sync::Mutex;
91/// use uni_common::sync::acquire_mutex;
92///
93/// let lock = Mutex::new(42);
94/// let guard = acquire_mutex(&lock, "my_data").unwrap();
95/// assert_eq!(*guard, 42);
96/// ```
97pub fn acquire_mutex<'a, T>(
98    lock: &'a Mutex<T>,
99    lock_name: &'static str,
100) -> Result<MutexGuard<'a, T>, LockPoisonedError> {
101    lock.lock()
102        .map_err(|_: PoisonError<_>| LockPoisonedError { lock_name })
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::sync::Arc;
109    use std::thread;
110
111    #[test]
112    fn test_acquire_read_success() {
113        let lock = RwLock::new(42);
114        let guard = acquire_read(&lock, "test").unwrap();
115        assert_eq!(*guard, 42);
116    }
117
118    #[test]
119    fn test_acquire_write_success() {
120        let lock = RwLock::new(42);
121        let mut guard = acquire_write(&lock, "test").unwrap();
122        *guard = 100;
123        drop(guard);
124
125        let guard = acquire_read(&lock, "test").unwrap();
126        assert_eq!(*guard, 100);
127    }
128
129    #[test]
130    fn test_acquire_mutex_success() {
131        let lock = Mutex::new(42);
132        let guard = acquire_mutex(&lock, "test").unwrap();
133        assert_eq!(*guard, 42);
134    }
135
136    #[test]
137    fn test_poisoned_mutex_returns_error() {
138        let lock = Arc::new(Mutex::new(42));
139        let lock_clone = Arc::clone(&lock);
140
141        // Spawn a thread that panics while holding the lock
142        let handle = thread::spawn(move || {
143            let _guard = lock_clone.lock().unwrap();
144            panic!("Intentional panic to poison the lock");
145        });
146
147        // Wait for the thread to finish (it will panic)
148        let _ = handle.join();
149
150        // Now the lock should be poisoned
151        let result = acquire_mutex(&lock, "test_mutex");
152        assert!(result.is_err());
153        assert_eq!(result.unwrap_err().lock_name, "test_mutex");
154    }
155}