use crate::error::{Mecha10Error, Result};
use crate::types::NodeConfig;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct DependencyGraph {
dependencies: HashMap<String, Vec<String>>,
dependents: HashMap<String, Vec<String>>,
nodes: HashSet<String>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
dependencies: HashMap::new(),
dependents: HashMap::new(),
nodes: HashSet::new(),
}
}
pub fn from_configs(configs: &[NodeConfig]) -> Result<Self> {
let mut graph = Self::new();
for config in configs {
graph.add_node(&config.id);
}
for config in configs {
for dep in &config.depends_on {
graph.add_dependency(&config.id, dep)?;
}
}
graph.validate()?;
Ok(graph)
}
pub fn add_node(&mut self, node_id: &str) {
self.nodes.insert(node_id.to_string());
self.dependencies.entry(node_id.to_string()).or_default();
self.dependents.entry(node_id.to_string()).or_default();
}
pub fn add_dependency(&mut self, node_id: &str, dependency: &str) -> Result<()> {
if !self.nodes.contains(node_id) {
return Err(Mecha10Error::Configuration(format!(
"Node '{}' not found in graph",
node_id
)));
}
if !self.nodes.contains(dependency) {
return Err(Mecha10Error::Configuration(format!(
"Dependency '{}' not found (required by '{}')",
dependency, node_id
)));
}
self.dependencies
.entry(node_id.to_string())
.or_default()
.push(dependency.to_string());
self.dependents
.entry(dependency.to_string())
.or_default()
.push(node_id.to_string());
Ok(())
}
pub fn validate(&self) -> Result<()> {
self.check_circular_dependencies()?;
for (node_id, deps) in &self.dependencies {
for dep in deps {
if !self.nodes.contains(dep) {
return Err(Mecha10Error::Configuration(format!(
"Node '{}' depends on undefined node '{}'",
node_id, dep
)));
}
}
}
Ok(())
}
fn check_circular_dependencies(&self) -> Result<()> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for node in &self.nodes {
if !visited.contains(node) && self.has_cycle_dfs(node, &mut visited, &mut rec_stack)? {
return Err(Mecha10Error::Configuration(format!(
"Circular dependency detected involving node '{}'",
node
)));
}
}
Ok(())
}
fn has_cycle_dfs(
&self,
node: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> Result<bool> {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
if let Some(deps) = self.dependencies.get(node) {
for dep in deps {
if !visited.contains(dep) {
if self.has_cycle_dfs(dep, visited, rec_stack)? {
return Ok(true);
}
} else if rec_stack.contains(dep) {
return Ok(true);
}
}
}
rec_stack.remove(node);
Ok(false)
}
pub fn topological_sort(&self) -> Result<Vec<String>> {
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut result = Vec::new();
let mut queue = VecDeque::new();
for node in &self.nodes {
let degree = self.dependencies.get(node).map(|d| d.len()).unwrap_or(0);
in_degree.insert(node.clone(), degree);
if degree == 0 {
queue.push_back(node.clone());
}
}
while let Some(node) = queue.pop_front() {
result.push(node.clone());
if let Some(dependents) = self.dependents.get(&node) {
for dependent in dependents {
if let Some(degree) = in_degree.get_mut(dependent) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dependent.clone());
}
}
}
}
}
if result.len() != self.nodes.len() {
return Err(Mecha10Error::Configuration(
"Circular dependency detected during topological sort".to_string(),
));
}
debug!("Topological sort result: {:?}", result);
Ok(result)
}
pub fn reverse_topological_sort(&self) -> Result<Vec<String>> {
let mut order = self.topological_sort()?;
order.reverse();
debug!("Reverse topological sort result: {:?}", order);
Ok(order)
}
pub fn startup_plan(&self) -> Result<StartupPlan> {
let mut levels: Vec<Vec<String>> = Vec::new();
let mut processed = HashSet::new();
let mut current_level = Vec::new();
for node in &self.nodes {
if self.dependencies.get(node).map(|d| d.is_empty()).unwrap_or(true) {
current_level.push(node.clone());
processed.insert(node.clone());
}
}
if !current_level.is_empty() {
levels.push(current_level.clone());
}
while processed.len() < self.nodes.len() {
let mut next_level = Vec::new();
for node in &self.nodes {
if processed.contains(node) {
continue;
}
if let Some(deps) = self.dependencies.get(node) {
if deps.iter().all(|dep| processed.contains(dep)) {
next_level.push(node.clone());
processed.insert(node.clone());
}
}
}
if next_level.is_empty() && processed.len() < self.nodes.len() {
return Err(Mecha10Error::Configuration(
"Circular dependency detected in startup plan".to_string(),
));
}
if !next_level.is_empty() {
levels.push(next_level);
}
}
info!("Startup plan created with {} levels", levels.len());
for (i, level) in levels.iter().enumerate() {
debug!(" Level {}: {:?}", i, level);
}
Ok(StartupPlan { levels })
}
pub fn shutdown_plan(&self) -> Result<ShutdownPlan> {
let startup = self.startup_plan()?;
let mut levels = startup.levels;
levels.reverse();
info!("Shutdown plan created with {} levels", levels.len());
for (i, level) in levels.iter().enumerate() {
debug!(" Level {}: {:?}", i, level);
}
Ok(ShutdownPlan { levels })
}
pub fn get_all_dependencies(&self, node_id: &str) -> HashSet<String> {
Self::transitive_closure(&self.dependencies, node_id)
}
pub fn get_all_dependents(&self, node_id: &str) -> HashSet<String> {
Self::transitive_closure(&self.dependents, node_id)
}
fn transitive_closure(edges: &HashMap<String, Vec<String>>, node_id: &str) -> HashSet<String> {
let mut result = HashSet::new();
let mut to_visit = vec![node_id.to_string()];
while let Some(current) = to_visit.pop() {
if let Some(deps) = edges.get(¤t) {
for dep in deps {
if result.insert(dep.clone()) {
to_visit.push(dep.clone());
}
}
}
}
result
}
pub fn can_start(&self, node_id: &str, running: &HashSet<String>) -> bool {
if let Some(deps) = self.dependencies.get(node_id) {
deps.iter().all(|dep| running.contains(dep))
} else {
true
}
}
pub fn can_stop(&self, node_id: &str, running: &HashSet<String>) -> bool {
if let Some(deps) = self.dependents.get(node_id) {
!deps.iter().any(|dep| running.contains(dep))
} else {
true
}
}
pub fn get_direct_dependents(&self, node_id: &str) -> Vec<String> {
Self::direct_edges(&self.dependents, node_id)
}
pub fn get_direct_dependencies(&self, node_id: &str) -> Vec<String> {
Self::direct_edges(&self.dependencies, node_id)
}
fn direct_edges(edges: &HashMap<String, Vec<String>>, node_id: &str) -> Vec<String> {
edges.get(node_id).cloned().unwrap_or_default()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn all_nodes(&self) -> Vec<String> {
self.nodes.iter().cloned().collect()
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartupPlan {
pub levels: Vec<Vec<String>>,
}
impl StartupPlan {
pub fn level_count(&self) -> usize {
level_count(&self.levels)
}
pub fn get_level(&self, level: usize) -> Option<&Vec<String>> {
self.levels.get(level)
}
pub fn node_count(&self) -> usize {
plan_node_count(&self.levels)
}
pub fn max_parallelism(&self) -> usize {
max_parallelism(&self.levels)
}
pub fn flatten(&self) -> Vec<String> {
flatten_levels(&self.levels)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShutdownPlan {
pub levels: Vec<Vec<String>>,
}
impl ShutdownPlan {
pub fn level_count(&self) -> usize {
level_count(&self.levels)
}
pub fn get_level(&self, level: usize) -> Option<&Vec<String>> {
self.levels.get(level)
}
pub fn node_count(&self) -> usize {
plan_node_count(&self.levels)
}
pub fn max_parallelism(&self) -> usize {
max_parallelism(&self.levels)
}
pub fn flatten(&self) -> Vec<String> {
flatten_levels(&self.levels)
}
}
fn level_count(levels: &[Vec<String>]) -> usize {
levels.len()
}
fn plan_node_count(levels: &[Vec<String>]) -> usize {
levels.iter().map(|l| l.len()).sum()
}
fn max_parallelism(levels: &[Vec<String>]) -> usize {
levels.iter().map(|l| l.len()).max().unwrap_or(0)
}
fn flatten_levels(levels: &[Vec<String>]) -> Vec<String> {
levels.iter().flatten().cloned().collect()
}
pub fn create_graph(nodes: &[(&str, Vec<&str>)]) -> Result<DependencyGraph> {
let mut graph = DependencyGraph::new();
for (node_id, _) in nodes {
graph.add_node(node_id);
}
for (node_id, deps) in nodes {
for dep in deps {
graph.add_dependency(node_id, dep)?;
}
}
graph.validate()?;
Ok(graph)
}