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}