use parking_lot::Mutex;
use std::backtrace::Backtrace;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::ThreadId;
use crate::implementation::LockMetadata;
use crate::synchronizer::{SynchronizerReadLock, SynchronizerType, SynchronizerWriteLock};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Mode {
Shared,
Exclusive,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Mode::Shared => f.write_str("shared (read)"),
Mode::Exclusive => f.write_str("exclusive (write)"),
}
}
}
pub(crate) struct LockId(AtomicU64);
static NEXT_LOCK_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_HELD_SEQ: AtomicU64 = AtomicU64::new(1);
impl LockId {
pub(crate) const fn unassigned() -> Self {
LockId(AtomicU64::new(0))
}
fn get(&self) -> u64 {
match self.0.load(Ordering::Relaxed) {
0 => {
let new = NEXT_LOCK_ID.fetch_add(1, Ordering::Relaxed);
match self
.0
.compare_exchange(0, new, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => new,
Err(existing) => existing,
}
}
id => id,
}
}
}
pub(crate) struct HeldToken {
thread: ThreadId,
seq: u64,
}
struct HeldEntry {
id: u64,
seq: u64,
mode: Mode,
}
#[derive(Default)]
struct DetectorState {
held: HashMap<ThreadId, Vec<HeldEntry>>,
edges: HashMap<u64, HashMap<u64, Backtrace>>,
}
static STATE: Mutex<Option<DetectorState>> = Mutex::new(None);
fn tracked_mode(sync_type: SynchronizerType, write: bool) -> Option<Mode> {
match sync_type {
SynchronizerType::Arc | SynchronizerType::Rcu => None,
SynchronizerType::RwLock => Some(if write { Mode::Exclusive } else { Mode::Shared }),
SynchronizerType::Mutex => Some(Mode::Exclusive),
}
}
pub(crate) fn check_blocking_acquire(
metadata: &LockMetadata,
sync_type: SynchronizerType,
write: bool,
) {
let Some(mode) = tracked_mode(sync_type, write) else {
return;
};
let id = metadata.lock_id.get();
let thread = std::thread::current().id();
let mut guard = STATE.lock();
let state = guard.get_or_insert_with(Default::default);
let mut held_ids: Vec<u64> = Vec::new();
let mut relock: Option<Mode> = None;
if let Some(held) = state.held.get(&thread) {
for entry in held {
if entry.id == id {
relock = Some(entry.mode);
}
if !held_ids.contains(&entry.id) {
held_ids.push(entry.id);
}
}
}
if let Some(held_mode) = relock {
let msg = relock_message(id, held_mode, mode);
drop(guard);
panic!("{}", msg);
}
for from in held_ids {
if let Some(targets) = state.edges.get(&from) {
if targets.contains_key(&id) {
continue;
}
}
if let Some(path) = find_path(&state.edges, id, from) {
let msg = cycle_message(state, from, id, mode, &path);
drop(guard);
panic!("{}", msg);
}
state
.edges
.entry(from)
.or_default()
.insert(id, Backtrace::force_capture());
}
}
fn register_held(metadata: &LockMetadata, mode: Mode) -> HeldToken {
let id = metadata.lock_id.get();
let thread = std::thread::current().id();
let seq = NEXT_HELD_SEQ.fetch_add(1, Ordering::Relaxed);
let mut guard = STATE.lock();
let state = guard.get_or_insert_with(Default::default);
state
.held
.entry(thread)
.or_default()
.push(HeldEntry { id, seq, mode });
HeldToken { thread, seq }
}
pub(crate) fn release_held(token: HeldToken) {
let mut guard = STATE.lock();
let Some(state) = guard.as_mut() else {
return;
};
if let Some(entries) = state.held.get_mut(&token.thread) {
if let Some(pos) = entries.iter().rposition(|e| e.seq == token.seq) {
entries.remove(pos);
}
if entries.is_empty() {
state.held.remove(&token.thread);
}
}
}
pub(crate) fn read_guard_token<T: ?Sized>(
lock: &SynchronizerReadLock<LockMetadata, T>,
) -> Option<HeldToken> {
match lock {
SynchronizerReadLock::Arc(..) | SynchronizerReadLock::ReadCopyUpdate(..) => None,
SynchronizerReadLock::RwLock(metadata, _) => Some(register_held(metadata, Mode::Shared)),
SynchronizerReadLock::Mutex(metadata, _) => Some(register_held(metadata, Mode::Exclusive)),
}
}
pub(crate) fn write_guard_token<T: ?Sized>(
lock: &SynchronizerWriteLock<LockMetadata, T>,
) -> Option<HeldToken> {
match lock {
SynchronizerWriteLock::Arc(..) | SynchronizerWriteLock::ReadCopyUpdate(..) => None,
SynchronizerWriteLock::RwLock(metadata, _) | SynchronizerWriteLock::Mutex(metadata, _) => {
Some(register_held(metadata, Mode::Exclusive))
}
}
}
fn find_path(edges: &HashMap<u64, HashMap<u64, Backtrace>>, from: u64, to: u64) -> Option<Vec<u64>> {
let mut parents: HashMap<u64, u64> = HashMap::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(from);
while let Some(node) = queue.pop_front() {
if node == to {
let mut path = vec![to];
let mut cur = to;
while cur != from {
cur = parents[&cur];
path.push(cur);
}
path.reverse();
return Some(path);
}
if let Some(targets) = edges.get(&node) {
for &next in targets.keys() {
if next != from && !parents.contains_key(&next) {
parents.insert(next, node);
queue.push_back(next);
}
}
}
}
None
}
fn render_backtrace(backtrace: &Backtrace) -> String {
let full = backtrace.to_string();
let mut frames: Vec<Vec<&str>> = Vec::new();
for line in full.lines() {
let is_frame_start = line
.trim_start()
.split_once(':')
.is_some_and(|(n, _)| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()));
if is_frame_start || frames.is_empty() {
frames.push(vec![line]);
} else {
frames.last_mut().expect("non-empty").push(line);
}
}
let is_internal = |f: &Vec<&str>| {
f.iter().any(|l| {
l.contains("backtrace") && !l.contains("__rust_begin_short_backtrace")
|| l.contains("keepcalm::")
})
};
let start = frames
.iter()
.position(|f| !is_internal(f))
.unwrap_or(0);
let first_scaffold = frames.iter().position(|f| {
f.iter()
.any(|l| l.contains("__rust_begin_short_backtrace") || l.contains("lang_start"))
});
let end = first_scaffold.unwrap_or(frames.len());
if start >= end {
return full;
}
frames[start..end]
.iter()
.flat_map(|f| f.iter().copied())
.collect::<Vec<_>>()
.join("\n")
}
fn relock_message(id: u64, held_mode: Mode, mode: Mode) -> String {
let consequence = if held_mode == Mode::Shared && mode == Mode::Shared {
"This may deadlock: parking_lot read locks are fair and will block a \
recursive read whenever a writer is waiting."
} else {
"This will deadlock."
};
format!(
"keepcalm deadlock detection: lock #{id} is already held by this thread!\n\
\x20 currently held as: {held_mode}\n\
\x20 attempting to re-acquire as: {mode}\n\
\x20 {consequence}\n\
\x20 Note that a projection shares the lock of its root container."
)
}
fn cycle_message(
state: &DetectorState,
held: u64,
acquiring: u64,
mode: Mode,
path: &[u64],
) -> String {
let mut msg = format!(
"keepcalm deadlock detection: lock-order cycle detected!\n\
\x20 this thread is about to acquire lock #{acquiring} ({mode}) while holding lock #{held},\n\
\x20 but the opposite ordering has already been observed:\n"
);
for pair in path.windows(2) {
let (from, to) = (pair[0], pair[1]);
let _ = write!(
msg,
"\n lock #{to} was acquired while lock #{from} was held, first observed at:\n"
);
match state.edges.get(&from).and_then(|m| m.get(&to)) {
Some(backtrace) => {
let _ = writeln!(msg, "{}", render_backtrace(backtrace));
}
None => msg.push_str(" <backtrace unavailable>\n"),
}
}
msg.push_str(
"\n Acquiring these locks in a consistent order on every code path will fix this.",
);
msg
}
#[cfg(test)]
mod test {
use crate::{Shared, SharedMut};
#[test]
fn consistent_order_is_silent() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
for _ in 0..3 {
let _ga = a.write();
let _gb = b.write();
}
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn inverted_order_panics() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
let _gb = b.write();
}
let _gb = b.write();
let _ga = a.write();
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn transitive_cycle_panics() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
let c = SharedMut::new(3);
{
let _ga = a.write();
let _gb = b.write();
}
{
let _gb = b.write();
let _gc = c.write();
}
let _gc = c.write();
let _ga = a.write();
}
#[test]
#[should_panic(expected = "already held by this thread")]
fn rwlock_reentrant_read_panics() {
let a = SharedMut::new(1);
let _g1 = a.read();
let _g2 = a.read();
}
#[test]
#[should_panic(expected = "already held by this thread")]
fn projection_shares_root_lock() {
let root = SharedMut::new(("hello".to_string(), 1usize));
let projected = root.project(crate::project!(x: (String, usize), x.0));
let _root_guard = root.read();
let _proj_guard = projected.read();
}
#[test]
#[should_panic(expected = "lock-order cycle detected")]
fn projection_participates_in_cycles() {
let a = SharedMut::new((1, 2));
let a0 = a.project(crate::project!(x: (i32, i32), x.0));
let b = SharedMut::new(3);
{
let _ga = a0.write();
let _gb = b.write();
}
let _gb = b.write();
let _ga = a.write();
}
#[test]
fn try_lock_is_exempt() {
let a = SharedMut::new(1);
let b = SharedMut::new(2);
{
let _ga = a.write();
let _gb = b.try_write().expect("uncontended");
}
{
let _gb = b.write();
let _ga = a.write();
}
let _gb = b.try_write().expect("uncontended");
let _ga = a.write();
}
#[test]
fn arc_and_rcu_are_exempt() {
let arc = Shared::new(1);
let rcu = SharedMut::new_rcu(2);
let a = SharedMut::new(3);
let _g1 = arc.read();
let _g2 = rcu.read();
let _g3 = rcu.write();
let _g4 = arc.read();
let _g5 = a.write();
let _g6 = rcu.write();
}
#[test]
fn cross_thread_drop_releases() {
let a = SharedMut::new(1);
let guard = a.write();
std::thread::scope(|s| {
s.spawn(move || drop(guard));
});
let _g = a.write();
}
#[test]
fn global_locks_are_tracked() {
use crate::SharedGlobalMut;
static GLOBAL: SharedGlobalMut<usize> = SharedGlobalMut::new(1);
let a = SharedMut::new(2);
for _ in 0..2 {
let _g1 = GLOBAL.write();
let _g2 = a.write();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g2 = a.write();
let _g1 = GLOBAL.write();
}));
let err = result.expect_err("expected a lock-order cycle panic");
let msg = err
.downcast_ref::<String>()
.expect("panic message should be a String");
assert!(msg.contains("lock-order cycle detected"), "message: {msg}");
}
}