use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
pub struct DeadlockDetectorGuard {
shutdown: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Drop for DeadlockDetectorGuard {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
if handle.join().is_err() {
}
}
}
}
pub fn init_deadlock_detector_with_config(
interval: Duration,
panic_on_deadlock: bool,
) -> DeadlockDetectorGuard {
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
let poll_step = Duration::from_millis(10).min(interval);
let handle = std::thread::Builder::new()
.name("deadlock-detector".into())
.spawn(move || {
loop {
let mut waited = Duration::ZERO;
while waited < interval {
if shutdown_clone.load(Ordering::Relaxed) {
return;
}
let step = poll_step.min(interval - waited);
std::thread::sleep(step);
waited += step;
}
if shutdown_clone.load(Ordering::Relaxed) {
return;
}
let deadlocks = parking_lot::deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
let mut report = format!("{} deadlock(s) detected!\n", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
report.push_str(&format!("Deadlock #{i}:\n"));
for t in threads {
report.push_str(&format!(
" Thread Id {:#?}\n Backtrace:\n{:#?}\n",
t.thread_id(),
t.backtrace()
));
}
}
eprintln!("{report}");
if panic_on_deadlock {
panic!("Deadlock detected! See error output above for details.");
}
}
})
.expect("failed to spawn deadlock detector thread");
DeadlockDetectorGuard {
shutdown,
handle: Some(handle),
}
}
pub fn init_deadlock_detector() -> DeadlockDetectorGuard {
init_deadlock_detector_with_config(Duration::from_secs(1), true)
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use serial_test::serial;
use std::collections::HashSet;
use std::sync::Arc;
use std::thread::ThreadId;
use std::time::Instant;
fn wait_for_cycle(target: &HashSet<ThreadId>, budget: Duration) -> bool {
let start = Instant::now();
while start.elapsed() < budget {
std::thread::sleep(Duration::from_millis(50));
for cycle in parking_lot::deadlock::check_deadlock() {
let cycle_threads: HashSet<ThreadId> =
cycle.iter().map(|t| t.thread_id()).collect();
if &cycle_threads == target {
return true;
}
}
}
false
}
#[test]
#[serial(deadlock_detection)]
fn test_detector_lifecycle_no_false_positives() {
let guard = init_deadlock_detector_with_config(Duration::from_millis(100), false);
let mutex = Arc::new(Mutex::new(0));
let m = mutex.clone();
let handle = std::thread::spawn(move || {
let mut val = m.lock();
*val += 1;
});
handle.join().unwrap();
assert_eq!(*mutex.lock(), 1);
std::thread::sleep(Duration::from_millis(350));
drop(guard);
}
#[test]
#[serial(deadlock_detection)]
fn test_deadlock_is_detected() {
use std::sync::Barrier;
let _ = parking_lot::deadlock::check_deadlock();
let lock_a = Arc::new(Mutex::new(()));
let lock_b = Arc::new(Mutex::new(()));
let barrier = Arc::new(Barrier::new(2));
let la1 = lock_a.clone();
let lb1 = lock_b.clone();
let b1 = barrier.clone();
let t1 = std::thread::spawn(move || {
let _a = la1.lock();
b1.wait();
let _b = lb1.lock();
});
let la2 = lock_a.clone();
let lb2 = lock_b.clone();
let b2 = barrier.clone();
let t2 = std::thread::spawn(move || {
let _b = lb2.lock();
b2.wait();
let _a = la2.lock();
});
let our_threads: HashSet<ThreadId> = [t1.thread().id(), t2.thread().id()].into();
let detected = wait_for_cycle(&our_threads, Duration::from_secs(10));
assert!(
detected,
"Expected our 2-thread AB/BA deadlock to be detected within 10 seconds. \
This likely means the deadlock_detection feature is not enabled on parking_lot, \
or another check_deadlock() caller consumed our deadlock state \
(all deadlock tests must share #[serial(deadlock_detection)], and the \
detector guard must join its thread on drop)."
);
}
fn spawn_deadlock_pair() -> HashSet<ThreadId> {
use std::sync::Barrier;
let lock_a = Arc::new(Mutex::new(()));
let lock_b = Arc::new(Mutex::new(()));
let barrier = Arc::new(Barrier::new(2));
let la1 = lock_a.clone();
let lb1 = lock_b.clone();
let b1 = barrier.clone();
let t1 = std::thread::spawn(move || {
let _a = la1.lock();
b1.wait();
let _b = lb1.lock();
});
let la2 = lock_a;
let lb2 = lock_b;
let b2 = barrier;
let t2 = std::thread::spawn(move || {
let _b = lb2.lock();
b2.wait();
let _a = la2.lock();
});
[t1.thread().id(), t2.thread().id()].into()
}
#[test]
#[serial(deadlock_detection)]
fn test_concurrent_deadlocks_are_each_detected() {
let _ = parking_lot::deadlock::check_deadlock();
let pair_one = spawn_deadlock_pair();
let pair_two = spawn_deadlock_pair();
let mut pairs = vec![pair_one, pair_two];
let mut both_in_one_call = false;
'attempts: for attempt in 0..20 {
std::thread::sleep(Duration::from_millis(300));
let deadlocks = parking_lot::deadlock::check_deadlock();
let cycles: Vec<HashSet<ThreadId>> = deadlocks
.iter()
.map(|cycle| cycle.iter().map(|t| t.thread_id()).collect())
.collect();
if pairs.iter().all(|p| cycles.iter().any(|c| c == p)) {
both_in_one_call = true;
break 'attempts;
}
if attempt < 19 {
pairs = vec![spawn_deadlock_pair(), spawn_deadlock_pair()];
}
}
assert!(
both_in_one_call,
"expected a single check_deadlock() call to report both independent \
deadlock cycles at once (this process-global contamination is what \
broke test_deadlock_is_detected under parallel execution, #3627)"
);
}
#[test]
#[serial(deadlock_detection)]
fn test_dropped_detector_does_not_consume_later_deadlock() {
let guard = init_deadlock_detector_with_config(Duration::from_millis(10), false);
let _ = parking_lot::deadlock::check_deadlock();
drop(guard);
let our_threads = spawn_deadlock_pair();
assert!(
wait_for_cycle(&our_threads, Duration::from_secs(10)),
"a dropped detector consumed our deadlock cycle — \
DeadlockDetectorGuard::drop must join the detector thread (#3627)"
);
}
}