use crate::Identity;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
tokio::task_local! {
pub(crate) static CURRENT_ACTOR: Identity;
}
static WAIT_FOR: OnceLock<Mutex<HashMap<u64, Vec<Identity>>>> = OnceLock::new();
fn wait_for_graph() -> &'static Mutex<HashMap<u64, Vec<Identity>>> {
WAIT_FOR.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) struct AskEdge {
caller: u64,
callee_id: u64,
removed: AtomicBool,
}
impl AskEdge {
fn remove_once(&self) {
if self.removed.load(Ordering::Relaxed) {
return;
}
let mut graph = wait_for_graph().lock().unwrap_or_else(|e| e.into_inner());
if self.removed.swap(true, Ordering::Relaxed) {
return;
}
remove_edge(&mut graph, self.caller, self.callee_id);
}
}
pub(crate) struct WaitForGuard {
edge: Arc<AskEdge>,
}
impl WaitForGuard {
pub(crate) fn token(&self) -> AskEdgeToken {
AskEdgeToken {
edge: self.edge.clone(),
}
}
}
impl Drop for WaitForGuard {
fn drop(&mut self) {
self.edge.remove_once();
}
}
pub(crate) struct AskEdgeToken {
edge: Arc<AskEdge>,
}
impl Drop for AskEdgeToken {
fn drop(&mut self) {
self.edge.remove_once();
}
}
fn remove_edge(graph: &mut HashMap<u64, Vec<Identity>>, caller: u64, callee_id: u64) {
if let Some(edges) = graph.get_mut(&caller) {
if let Some(pos) = edges.iter().position(|c| c.id == callee_id) {
edges.swap_remove(pos);
}
if edges.is_empty() {
graph.remove(&caller);
}
}
}
#[must_use = "dropping the guard immediately removes the wait-for edge, disabling detection for this ask"]
pub(crate) fn register_ask_edge(callee: Identity, operation: &str) -> Option<WaitForGuard> {
let caller = CURRENT_ACTOR.try_with(|id| *id).ok()?;
let mut graph = wait_for_graph().lock().unwrap_or_else(|e| e.into_inner());
if caller.id == callee.id
|| (graph.contains_key(&callee.id) && has_path(&graph, callee.id, caller.id))
{
let cycle = format_cycle_path(&graph, caller, callee);
drop(graph);
panic!(
"Deadlock detected: {operation} cycle {cycle}\n\
This is a design error. Use `tell` to break the cycle, \
or restructure actor dependencies."
);
}
graph.entry(caller.id).or_default().push(callee);
Some(WaitForGuard {
edge: Arc::new(AskEdge {
caller: caller.id,
callee_id: callee.id,
removed: AtomicBool::new(false),
}),
})
}
fn has_path(graph: &HashMap<u64, Vec<Identity>>, from: u64, to: u64) -> bool {
let mut stack = vec![from];
let mut visited = HashSet::new();
while let Some(current) = stack.pop() {
if current == to {
return true;
}
if !visited.insert(current) {
continue;
}
if let Some(edges) = graph.get(¤t) {
for callee in edges {
stack.push(callee.id);
}
}
}
false
}
fn format_cycle_path(
graph: &HashMap<u64, Vec<Identity>>,
caller: Identity,
callee: Identity,
) -> String {
if caller.id == callee.id {
return format!("{caller} -> {caller}");
}
let mut path = vec![caller.to_string()];
match find_path(graph, callee, caller.id) {
Some(nodes) => path.extend(nodes.iter().map(|id| id.to_string())),
None => path.push(callee.to_string()),
}
path.join(" -> ")
}
fn find_path(
graph: &HashMap<u64, Vec<Identity>>,
start: Identity,
target: u64,
) -> Option<Vec<Identity>> {
let mut stack: Vec<Vec<Identity>> = vec![vec![start]];
let mut visited = HashSet::new();
while let Some(path) = stack.pop() {
let current = *path.last().expect("path is never empty");
if current.id == target {
return Some(path);
}
if !visited.insert(current.id) {
continue;
}
if let Some(edges) = graph.get(¤t.id) {
for callee in edges {
let mut next = path.clone();
next.push(*callee);
stack.push(next);
}
}
}
None
}
#[cfg(test)]
mod deadlock_graph_tests {
use super::{find_path, format_cycle_path, has_path, remove_edge, Identity};
use std::collections::HashMap;
fn id(n: u64) -> Identity {
let name: &'static str = match n {
1 => "A",
2 => "B",
3 => "C",
4 => "D",
_ => "X",
};
Identity::new(n, name)
}
fn graph_of(edges: &[(u64, &[u64])]) -> HashMap<u64, Vec<Identity>> {
let mut g = HashMap::new();
for (caller, callees) in edges {
g.insert(*caller, callees.iter().map(|c| id(*c)).collect());
}
g
}
#[test]
fn has_path_linear_chain() {
let g = graph_of(&[(1, &[2]), (2, &[3])]);
assert!(has_path(&g, 1, 3));
assert!(has_path(&g, 1, 2));
assert!(!has_path(&g, 3, 1));
}
#[test]
fn has_path_follows_every_branch() {
let g = graph_of(&[(1, &[2, 3]), (3, &[4])]);
assert!(has_path(&g, 1, 4), "must reach 4 through the second branch");
assert!(has_path(&g, 1, 2));
assert!(!has_path(&g, 2, 4), "no edge out of 2");
}
#[test]
fn has_path_terminates_on_preexisting_cycle() {
let g = graph_of(&[(1, &[2]), (2, &[1])]);
assert!(!has_path(&g, 1, 99));
assert!(has_path(&g, 1, 2));
}
#[test]
fn find_path_reconstructs_identities() {
let g = graph_of(&[(2, &[3]), (3, &[1])]);
let path = find_path(&g, id(2), 1).expect("path 2->3->1 exists");
let ids: Vec<u64> = path.iter().map(|i| i.id).collect();
assert_eq!(ids, vec![2, 3, 1]);
}
#[test]
fn format_cycle_path_renders_full_cycle() {
let g = graph_of(&[(2, &[1])]);
let rendered = format_cycle_path(&g, id(1), id(2));
assert_eq!(rendered, "A(#1) -> B(#2) -> A(#1)");
}
#[test]
fn format_cycle_path_self_ask() {
let g = HashMap::new();
assert_eq!(format_cycle_path(&g, id(1), id(1)), "A(#1) -> A(#1)");
}
#[test]
fn remove_edge_drops_only_one_distinct_edge() {
let mut g = graph_of(&[(1, &[2, 3])]);
remove_edge(&mut g, 1, 2);
let remaining: Vec<u64> = g.get(&1).unwrap().iter().map(|i| i.id).collect();
assert_eq!(remaining, vec![3]);
}
#[test]
fn remove_edge_is_multiset_for_duplicate_targets() {
let mut g = graph_of(&[(1, &[2, 2])]);
remove_edge(&mut g, 1, 2);
let remaining: Vec<u64> = g.get(&1).unwrap().iter().map(|i| i.id).collect();
assert_eq!(remaining, vec![2]);
}
#[test]
fn remove_edge_clears_empty_key() {
let mut g = graph_of(&[(1, &[2])]);
remove_edge(&mut g, 1, 2);
assert!(
!g.contains_key(&1),
"key removed once its last edge is gone"
);
}
#[test]
fn remove_edge_missing_is_noop() {
let mut g = graph_of(&[(1, &[2])]);
remove_edge(&mut g, 1, 99); remove_edge(&mut g, 42, 2); assert_eq!(g.get(&1).unwrap().len(), 1);
}
}