Skip to main content

dev_async/
deadlock.rs

1//! Timeout-based deadlock detection helpers.
2//!
3//! `try_lock_with_timeout` wraps an async lock acquisition with a hard
4//! deadline. If the lock cannot be acquired in time, the result is
5//! [`Verdict::Fail`] with a `deadlock_suspected` tag.
6//!
7//! ## What this catches
8//!
9//! - Real deadlock cycles (lock A waits for B, B waits for A).
10//! - Single-lock starvation (one holder never releases).
11//! - Long contention periods that look like deadlocks from the caller's
12//!   perspective.
13//!
14//! ## What this does NOT catch
15//!
16//! Timeout-based detection cannot distinguish a true deadlock from a
17//! slow-but-eventually-completing operation. For deterministic cycle
18//! detection you'd need a lock-graph tracker, which is out of scope
19//! for `0.x`.
20//!
21//! [`Verdict::Fail`]: dev_report::Verdict::Fail
22
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use dev_report::{CheckResult, Evidence, Severity};
27use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
28
29/// Acquire a `tokio::sync::Mutex` lock or fail with a deadlock-suspected
30/// verdict.
31///
32/// On success, returns `Ok((CheckResult::pass, MutexGuard))`. On
33/// timeout, returns `Err(CheckResult::fail)` and the lock is left
34/// alone.
35///
36/// # Example
37///
38/// ```no_run
39/// use dev_async::deadlock::try_mutex_lock_with_timeout;
40/// use std::sync::Arc;
41/// use std::time::Duration;
42/// use tokio::sync::Mutex;
43///
44/// # async fn ex() {
45/// let m = Arc::new(Mutex::new(0));
46/// match try_mutex_lock_with_timeout("counter", &m, Duration::from_millis(50)).await {
47///     Ok((check, mut guard)) => {
48///         *guard += 1;
49///         drop(guard);
50///         assert!(check.has_tag("async"));
51///     }
52///     Err(check) => {
53///         assert!(check.has_tag("deadlock_suspected"));
54///     }
55/// };
56/// # }
57/// ```
58pub async fn try_mutex_lock_with_timeout<'a, T>(
59    name: impl Into<String>,
60    lock: &'a Arc<Mutex<T>>,
61    timeout: Duration,
62) -> Result<(CheckResult, MutexGuard<'a, T>), CheckResult> {
63    let name = name.into();
64    let started = Instant::now();
65    match tokio::time::timeout(timeout, lock.lock()).await {
66        Ok(guard) => {
67            let elapsed = started.elapsed();
68            let mut c = CheckResult::pass(format!("async::lock::{name}"))
69                .with_duration_ms(elapsed.as_millis() as u64);
70            c.tags = vec!["async".to_string(), "lock".to_string()];
71            c.evidence = vec![
72                Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
73                Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
74            ];
75            Ok((c, guard))
76        }
77        Err(_) => {
78            let mut c = CheckResult::fail(format!("async::lock::{name}"), Severity::Error)
79                .with_detail(format!("could not acquire lock within {timeout:?}"));
80            c.tags = vec![
81                "async".to_string(),
82                "lock".to_string(),
83                "deadlock_suspected".to_string(),
84                "regression".to_string(),
85            ];
86            c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
87            Err(c)
88        }
89    }
90}
91
92/// Acquire a `tokio::sync::RwLock` read lock or fail.
93pub async fn try_rwlock_read_with_timeout<'a, T>(
94    name: impl Into<String>,
95    lock: &'a Arc<RwLock<T>>,
96    timeout: Duration,
97) -> Result<(CheckResult, RwLockReadGuard<'a, T>), CheckResult> {
98    let name = name.into();
99    let started = Instant::now();
100    match tokio::time::timeout(timeout, lock.read()).await {
101        Ok(guard) => {
102            let elapsed = started.elapsed();
103            let mut c = CheckResult::pass(format!("async::lock::{name}::read"))
104                .with_duration_ms(elapsed.as_millis() as u64);
105            c.tags = vec!["async".to_string(), "lock".to_string()];
106            c.evidence = vec![
107                Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
108                Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
109            ];
110            Ok((c, guard))
111        }
112        Err(_) => {
113            let mut c = CheckResult::fail(format!("async::lock::{name}::read"), Severity::Error)
114                .with_detail(format!("could not acquire read lock within {timeout:?}"));
115            c.tags = vec![
116                "async".to_string(),
117                "lock".to_string(),
118                "deadlock_suspected".to_string(),
119                "regression".to_string(),
120            ];
121            c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
122            Err(c)
123        }
124    }
125}
126
127/// Acquire a `tokio::sync::RwLock` write lock or fail.
128pub async fn try_rwlock_write_with_timeout<'a, T>(
129    name: impl Into<String>,
130    lock: &'a Arc<RwLock<T>>,
131    timeout: Duration,
132) -> Result<(CheckResult, RwLockWriteGuard<'a, T>), CheckResult> {
133    let name = name.into();
134    let started = Instant::now();
135    match tokio::time::timeout(timeout, lock.write()).await {
136        Ok(guard) => {
137            let elapsed = started.elapsed();
138            let mut c = CheckResult::pass(format!("async::lock::{name}::write"))
139                .with_duration_ms(elapsed.as_millis() as u64);
140            c.tags = vec!["async".to_string(), "lock".to_string()];
141            c.evidence = vec![
142                Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
143                Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
144            ];
145            Ok((c, guard))
146        }
147        Err(_) => {
148            let mut c = CheckResult::fail(format!("async::lock::{name}::write"), Severity::Error)
149                .with_detail(format!("could not acquire write lock within {timeout:?}"));
150            c.tags = vec![
151                "async".to_string(),
152                "lock".to_string(),
153                "deadlock_suspected".to_string(),
154                "regression".to_string(),
155            ];
156            c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
157            Err(c)
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use dev_report::Verdict;
166
167    #[tokio::test]
168    async fn mutex_lock_acquires_under_timeout() {
169        let m = Arc::new(Mutex::new(0));
170        let (check, _g) = try_mutex_lock_with_timeout("a", &m, Duration::from_millis(50))
171            .await
172            .unwrap();
173        assert_eq!(check.verdict, Verdict::Pass);
174        assert!(check.has_tag("lock"));
175    }
176
177    #[tokio::test]
178    async fn mutex_lock_times_out_when_held() {
179        let m = Arc::new(Mutex::new(0));
180        let _held = m.lock().await;
181        let err = try_mutex_lock_with_timeout("a", &m, Duration::from_millis(20))
182            .await
183            .unwrap_err();
184        assert_eq!(err.verdict, Verdict::Fail);
185        assert!(err.has_tag("deadlock_suspected"));
186        assert!(err.has_tag("regression"));
187    }
188
189    #[tokio::test]
190    async fn rwlock_read_under_timeout() {
191        let l = Arc::new(RwLock::new(0));
192        let (check, _g) = try_rwlock_read_with_timeout("a", &l, Duration::from_millis(50))
193            .await
194            .unwrap();
195        assert_eq!(check.verdict, Verdict::Pass);
196    }
197
198    #[tokio::test]
199    async fn rwlock_write_times_out_when_held() {
200        let l = Arc::new(RwLock::new(0));
201        let _held = l.write().await;
202        let err = try_rwlock_write_with_timeout("a", &l, Duration::from_millis(20))
203            .await
204            .unwrap_err();
205        assert_eq!(err.verdict, Verdict::Fail);
206    }
207}