use std::any::TypeId;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use parking_lot::RwLock;
use crate::{Context, FiberId};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DependencyGraph {
adjacency: BTreeMap<FiberId, Vec<FiberId>>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, id: FiberId) {
self.adjacency.entry(id).or_default();
}
pub fn add_edge(&mut self, from: FiberId, to: FiberId) {
let targets = self.adjacency.entry(from).or_default();
if !targets.contains(&to) {
targets.push(to);
}
}
pub fn node_ids(&self) -> impl Iterator<Item = FiberId> + '_ {
self.adjacency.keys().copied()
}
pub fn neighbors(&self, id: FiberId) -> &[FiberId] {
self.adjacency.get(&id).map(Vec::as_slice).unwrap_or(&[])
}
pub fn len(&self) -> usize {
self.adjacency.len()
}
pub fn is_empty(&self) -> bool {
self.adjacency.is_empty()
}
}
#[derive(Clone, Copy, PartialEq)]
enum Color {
White,
Gray,
Black,
}
pub fn find_dependency_cycle(graph: &DependencyGraph) -> Option<Vec<FiberId>> {
find_dependency_cycles(graph).into_iter().next()
}
pub fn find_dependency_cycles(graph: &DependencyGraph) -> Vec<Vec<FiberId>> {
let mut color: HashMap<FiberId, Color> = graph.node_ids().map(|n| (n, Color::White)).collect();
let mut stack: Vec<(FiberId, usize)> = Vec::new();
let mut found: BTreeSet<Vec<FiberId>> = BTreeSet::new();
for root in graph.node_ids() {
if color.get(&root) != Some(&Color::White) {
continue;
}
color.insert(root, Color::Gray);
stack.push((root, 0));
while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
let neighbors = graph.neighbors(node).to_vec();
if *idx >= neighbors.len() {
color.insert(node, Color::Black);
stack.pop();
if let Some(parent) = stack.last_mut() {
parent.1 += 1;
}
continue;
}
let next = neighbors[*idx];
let back_edge = matches!(color.get(&next), Some(Color::Gray));
if back_edge {
found.insert(canonical_cycle(gray_stack_cycle(&stack, next)));
stack.last_mut().expect("non-empty").1 += 1;
continue;
}
match color.get(&next) {
Some(Color::Black) => {
stack.last_mut().expect("non-empty").1 += 1;
}
_ => {
color.insert(next, Color::Gray);
stack.push((next, 0));
}
}
}
}
found.into_iter().collect()
}
fn gray_stack_cycle(stack: &[(FiberId, usize)], back_edge_target: FiberId) -> Vec<FiberId> {
let start = stack
.iter()
.rposition(|(id, _)| *id == back_edge_target)
.unwrap_or(0);
let mut cycle: Vec<FiberId> = stack[start..].iter().map(|(id, _)| *id).collect();
cycle.push(back_edge_target);
cycle
}
fn canonical_cycle(closed: Vec<FiberId>) -> Vec<FiberId> {
let ring = closed.len().saturating_sub(1);
let mut best: Option<Vec<FiberId>> = None;
for shift in 0..ring.max(1) {
let rotated: Vec<FiberId> = (0..ring)
.map(|i| closed[(shift + i) % ring.max(1)])
.chain(std::iter::once(closed[shift % ring.max(1)]))
.collect();
best = Some(match best {
None => rotated,
Some(b) if rotated < b => rotated,
Some(b) => b,
});
}
best.unwrap_or(closed)
}
#[derive(Default)]
pub(crate) struct CycleLedger {
providers: RwLock<HashMap<(TypeId, Option<String>), FiberId>>,
fibers: RwLock<BTreeMap<FiberId, String>>,
}
impl CycleLedger {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn record_provider(&self, tid: TypeId, label: Option<&str>, fid: FiberId) {
self.providers
.write()
.insert((tid, label.map(str::to_string)), fid);
}
pub(crate) fn note_entry(&self, fid: FiberId, entry_id: &str) {
self.fibers.write().insert(fid, entry_id.to_string());
}
pub(crate) fn provider_of(&self, tid: TypeId, label: Option<&str>) -> Option<FiberId> {
self.providers
.read()
.get(&(tid, label.map(str::to_string)))
.copied()
}
pub(crate) fn provider_fibers(&self) -> Vec<FiberId> {
let mut ids: BTreeSet<FiberId> = self.providers.read().values().copied().collect();
ids.extend(self.fibers.read().keys().copied());
ids.into_iter().collect()
}
pub(crate) fn entry_id_of(&self, fid: FiberId) -> Option<String> {
self.fibers.read().get(&fid).cloned()
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.providers.read().len()
}
}
impl crate::Service for CycleLedger {}
pub(crate) fn build_dependency_graph(ctx: &Arc<Context>) -> Option<DependencyGraph> {
let ledger = ctx.get::<CycleLedger>()?;
let registry = ctx.get::<crate::RegistryService>()?;
let mut graph = DependencyGraph::new();
for fid in ledger.provider_fibers() {
graph.add_node(fid);
let Some(fiber) = registry.get_fiber(fid) else {
continue;
};
for tid in fiber.injected_type_ids() {
let label = ctx.isolate_label(tid);
if let Some(provider) = ledger.provider_of(tid, label.as_deref()) {
if provider != fid {
graph.add_edge(fid, provider);
}
}
}
}
Some(graph)
}
#[cfg(test)]
mod tests {
use super::*;
fn graph_from_edges(edges: &[(FiberId, FiberId)]) -> DependencyGraph {
let mut g = DependencyGraph::new();
for &(from, to) in edges {
g.add_edge(from, to);
}
g
}
#[test]
fn empty_and_single_node_graphs_have_no_cycle() {
assert!(find_dependency_cycle(&DependencyGraph::new()).is_none());
assert!(find_dependency_cycles(&DependencyGraph::new()).is_empty());
let mut g = DependencyGraph::new();
g.add_node(1);
assert!(find_dependency_cycle(&g).is_none());
}
#[test]
fn acyclic_chain_has_no_cycle() {
let g = graph_from_edges(&[(1, 2), (2, 3), (3, 4)]);
assert!(find_dependency_cycle(&g).is_none());
}
#[test]
fn diamond_is_acyclic() {
let g = graph_from_edges(&[(1, 2), (1, 3), (2, 4), (3, 4)]);
assert!(find_dependency_cycle(&g).is_none());
}
#[test]
fn two_node_cycle_is_found_in_edge_direction() {
let g = graph_from_edges(&[(1, 2), (2, 1)]);
assert_eq!(find_dependency_cycle(&g), Some(vec![1, 2, 1]));
}
#[test]
fn three_node_cycle_is_found() {
let g = graph_from_edges(&[(10, 20), (20, 30), (30, 10)]);
assert_eq!(find_dependency_cycle(&g), Some(vec![10, 20, 30, 10]));
}
#[test]
fn cycle_nested_behind_acyclic_prefix_is_found() {
let g = graph_from_edges(&[(1, 2), (2, 3), (2, 4), (3, 4), (4, 3)]);
let found = find_dependency_cycle(&g).expect("cycle exists");
assert_eq!(&found[..], &[3, 4, 3]);
}
#[test]
fn self_loop_reports_two_node_path() {
let g = graph_from_edges(&[(7, 7)]);
assert_eq!(find_dependency_cycle(&g), Some(vec![7, 7]));
}
#[test]
fn disconnected_components_only_yield_their_own_cycle() {
let g = graph_from_edges(&[(1, 2), (2, 3), (4, 5), (5, 4)]);
assert_eq!(find_dependency_cycle(&g), Some(vec![4, 5, 4]));
}
#[test]
fn multiple_disjoint_cycles_are_all_reported_once() {
let g = graph_from_edges(&[(1, 2), (2, 1), (5, 4), (4, 5), (3, 3)]);
let found = find_dependency_cycles(&g);
assert_eq!(found, vec![vec![1, 2, 1], vec![3, 3], vec![4, 5, 4]]);
assert_eq!(find_dependency_cycle(&g), Some(vec![1, 2, 1]));
}
#[test]
fn duplicate_edges_do_not_confuse_the_walk() {
let mut g = DependencyGraph::new();
g.add_edge(1, 2);
g.add_edge(1, 2);
g.add_edge(2, 1);
assert_eq!(find_dependency_cycle(&g), Some(vec![1, 2, 1]));
assert_eq!(g.neighbors(1), &[2]);
}
#[test]
fn cross_edges_into_explored_subtrees_stay_acyclic() {
let g = graph_from_edges(&[(1, 2), (2, 3), (1, 3), (3, 4)]);
assert!(find_dependency_cycle(&g).is_none());
}
#[test]
fn ledger_round_trips_provider_lookup() {
use crate::Service;
#[derive(Debug)]
struct Probe;
impl Service for Probe {}
let ledger = CycleLedger::new();
let tid = TypeId::of::<Probe>();
assert!(ledger.provider_of(tid, None).is_none());
ledger.record_provider(tid, None, 11);
ledger.record_provider(tid, Some("tenant:acme"), 12);
ledger.note_entry(11, "svc:a");
assert_eq!(ledger.provider_of(tid, None), Some(11));
assert_eq!(ledger.provider_of(tid, Some("tenant:acme")), Some(12));
assert_eq!(ledger.provider_of(tid, Some("tenant:other")), None);
ledger.record_provider(tid, None, 13);
assert_eq!(ledger.provider_of(tid, None), Some(13));
assert_eq!(ledger.len(), 2);
assert_eq!(ledger.entry_id_of(11).as_deref(), Some("svc:a"));
assert_eq!(ledger.entry_id_of(999), None);
assert_eq!(ledger.provider_fibers(), vec![11, 12, 13]);
}
#[test]
fn sorted_node_enumeration_keeps_results_deterministic() {
let mut g = DependencyGraph::new();
g.add_node(9);
g.add_node(2);
g.add_node(5);
let nodes: BTreeSet<_> = g.node_ids().collect();
assert_eq!(nodes, BTreeSet::from([2, 5, 9]));
assert_eq!(g.len(), 3);
assert!(!g.is_empty());
}
}