use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::{DbxError, DbxResult};
#[derive(Debug, Clone)]
pub enum JobSchedule {
Interval(std::time::Duration),
After(String),
Manual,
}
pub struct JobDag {
jobs: HashMap<String, JobSchedule>,
dependents: HashMap<String, Vec<String>>,
prerequisites: HashMap<String, Vec<String>>,
}
impl JobDag {
pub fn new() -> Self {
Self {
jobs: HashMap::new(),
dependents: HashMap::new(),
prerequisites: HashMap::new(),
}
}
pub fn add_job(&mut self, id: &str, schedule: JobSchedule) {
self.jobs.insert(id.to_string(), schedule);
self.dependents.entry(id.to_string()).or_default();
self.prerequisites.entry(id.to_string()).or_default();
}
pub fn add_dependency(&mut self, job: &str, depends_on: &str) {
self.dependents
.entry(depends_on.to_string())
.or_default()
.push(job.to_string());
self.prerequisites
.entry(job.to_string())
.or_default()
.push(depends_on.to_string());
self.dependents.entry(job.to_string()).or_default();
self.prerequisites
.entry(depends_on.to_string())
.or_default();
}
pub fn resolve_execution_order(&self) -> DbxResult<Vec<String>> {
let mut all_nodes: HashSet<String> = self.jobs.keys().cloned().collect();
for (k, vs) in &self.dependents {
all_nodes.insert(k.clone());
all_nodes.extend(vs.iter().cloned());
}
let mut in_degree: HashMap<String, usize> =
all_nodes.iter().map(|n| (n.clone(), 0)).collect();
for (job, prereqs) in &self.prerequisites {
*in_degree.entry(job.clone()).or_insert(0) += prereqs.len();
}
let mut queue: VecDeque<String> = in_degree
.iter()
.filter(|(_, v)| **v == 0)
.map(|(k, _)| k.clone())
.collect();
let mut sorted: Vec<String> = queue.drain(..).collect();
sorted.sort();
let mut queue: VecDeque<String> = VecDeque::from(sorted);
let mut order = Vec::new();
while let Some(job) = queue.pop_front() {
order.push(job.clone());
if let Some(deps) = self.dependents.get(&job) {
let mut next_batch: Vec<String> = Vec::new();
for d in deps {
let cnt = in_degree.entry(d.clone()).or_insert(0);
if *cnt > 0 {
*cnt -= 1;
}
if *cnt == 0 {
next_batch.push(d.clone());
}
}
next_batch.sort();
queue.extend(next_batch);
}
}
if order.len() != all_nodes.len() {
return Err(DbxError::InvalidArguments("순환 의존성 감지됨".to_string()));
}
Ok(order)
}
pub fn job_count(&self) -> usize {
self.jobs.len()
}
pub fn prerequisites_of(&self, job: &str) -> Vec<String> {
self.prerequisites.get(job).cloned().unwrap_or_default()
}
}
impl Default for JobDag {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_dag_dependency_ordering() {
let mut dag = JobDag::new();
dag.add_job(
"backup",
JobSchedule::Interval(std::time::Duration::from_secs(3600)),
);
dag.add_job("cleanup", JobSchedule::After("backup".to_string()));
dag.add_dependency("cleanup", "backup");
let order = dag.resolve_execution_order().unwrap();
let backup_pos = order.iter().position(|x| x == "backup").unwrap();
let cleanup_pos = order.iter().position(|x| x == "cleanup").unwrap();
assert!(backup_pos < cleanup_pos, "backup must run before cleanup");
}
#[test]
fn test_job_dag_cycle_detection() {
let mut dag = JobDag::new();
dag.add_dependency("a", "b");
dag.add_dependency("b", "a");
assert!(
dag.resolve_execution_order().is_err(),
"순환 의존성 감지 실패"
);
}
#[test]
fn test_job_dag_no_dependencies() {
let mut dag = JobDag::new();
dag.add_job("job1", JobSchedule::Manual);
dag.add_job("job2", JobSchedule::Manual);
dag.add_job("job3", JobSchedule::Manual);
let order = dag.resolve_execution_order().unwrap();
assert_eq!(order.len(), 3);
}
#[test]
fn test_job_dag_chain() {
let mut dag = JobDag::new();
dag.add_job("a", JobSchedule::Manual);
dag.add_job("b", JobSchedule::After("a".to_string()));
dag.add_job("c", JobSchedule::After("b".to_string()));
dag.add_dependency("b", "a");
dag.add_dependency("c", "b");
let order = dag.resolve_execution_order().unwrap();
assert_eq!(order.len(), 3);
let a_pos = order.iter().position(|x| x == "a").unwrap();
let b_pos = order.iter().position(|x| x == "b").unwrap();
let c_pos = order.iter().position(|x| x == "c").unwrap();
assert!(a_pos < b_pos);
assert!(b_pos < c_pos);
}
#[test]
fn test_job_dag_diamond() {
let mut dag = JobDag::new();
dag.add_job("a", JobSchedule::Manual);
dag.add_job("b", JobSchedule::After("a".to_string()));
dag.add_job("c", JobSchedule::After("a".to_string()));
dag.add_job("d", JobSchedule::Manual);
dag.add_dependency("b", "a");
dag.add_dependency("c", "a");
dag.add_dependency("d", "b");
dag.add_dependency("d", "c");
let order = dag.resolve_execution_order().unwrap();
assert_eq!(order.len(), 4);
let a_pos = order.iter().position(|x| x == "a").unwrap();
let b_pos = order.iter().position(|x| x == "b").unwrap();
let c_pos = order.iter().position(|x| x == "c").unwrap();
let d_pos = order.iter().position(|x| x == "d").unwrap();
assert!(a_pos < b_pos);
assert!(a_pos < c_pos);
assert!(b_pos < d_pos);
assert!(c_pos < d_pos);
}
}