use std::collections::VecDeque;
use std::sync::{Condvar, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct JobId(usize);
type Action = Box<dyn FnOnce() + Send>;
struct JobEntry {
name: &'static str,
action: Option<Action>,
dependencies: Vec<JobId>,
}
#[derive(Default)]
pub struct JobGraph {
jobs: Vec<JobEntry>,
}
impl JobGraph {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.jobs.len()
}
pub fn is_empty(&self) -> bool {
self.jobs.is_empty()
}
pub fn job_name(&self, id: JobId) -> &'static str {
self.jobs[id.0].name
}
pub fn add_job(
&mut self,
name: &'static str,
dependencies: &[JobId],
action: impl FnOnce() + Send + 'static,
) -> JobId {
let id = JobId(self.jobs.len());
self.jobs.push(JobEntry {
name,
action: Some(Box::new(action)),
dependencies: dependencies.to_vec(),
});
id
}
}
struct SchedulerState {
indegree: Vec<usize>,
dependents: Vec<Vec<usize>>,
ready: VecDeque<usize>,
remaining: usize,
}
#[derive(Debug)]
pub struct Scheduler {
pub worker_count: usize,
}
impl Scheduler {
pub fn new(worker_count: usize) -> Self {
Self {
worker_count: worker_count.max(1),
}
}
pub fn run(&self, mut graph: JobGraph) {
let n = graph.jobs.len();
if n == 0 {
return;
}
let mut indegree = vec![0usize; n];
let mut dependents: Vec<Vec<usize>> = (0..n).map(|_| Vec::new()).collect();
for (i, job) in graph.jobs.iter().enumerate() {
for dep in &job.dependencies {
assert!(
dep.0 < n,
"Scheduler::run: JobId({}) doesn't belong to this JobGraph",
dep.0
);
indegree[i] += 1;
dependents[dep.0].push(i);
}
}
assert!(
!has_cycle(&indegree, &dependents, n),
"Scheduler::run: dependency cycle in JobGraph"
);
let actions: Mutex<Vec<Option<Action>>> =
Mutex::new(graph.jobs.iter_mut().map(|j| j.action.take()).collect());
let ready: VecDeque<usize> = (0..n).filter(|&i| indegree[i] == 0).collect();
let state = Mutex::new(SchedulerState {
indegree,
dependents,
ready,
remaining: n,
});
let cvar = Condvar::new();
std::thread::scope(|scope| {
for _ in 0..self.worker_count {
scope.spawn(|| worker_loop(&state, &cvar, &actions));
}
});
}
}
fn worker_loop(
state: &Mutex<SchedulerState>,
cvar: &Condvar,
actions: &Mutex<Vec<Option<Action>>>,
) {
loop {
let job_index = {
let mut guard = state.lock().unwrap();
loop {
if let Some(i) = guard.ready.pop_front() {
break Some(i);
}
if guard.remaining == 0 {
break None;
}
guard = cvar.wait(guard).unwrap();
}
};
let Some(i) = job_index else {
break;
};
let action = actions.lock().unwrap()[i].take();
if let Some(action) = action {
action();
}
let mut guard = state.lock().unwrap();
let newly_ready: Vec<usize> = std::mem::take(&mut guard.dependents[i])
.into_iter()
.filter(|&d| {
guard.indegree[d] -= 1;
guard.indegree[d] == 0
})
.collect();
guard.ready.extend(newly_ready);
guard.remaining -= 1;
drop(guard);
cvar.notify_all();
}
}
fn has_cycle(indegree: &[usize], dependents: &[Vec<usize>], n: usize) -> bool {
let mut indegree = indegree.to_vec();
let mut queue: VecDeque<usize> = (0..n).filter(|&i| indegree[i] == 0).collect();
let mut visited = 0usize;
while let Some(i) = queue.pop_front() {
visited += 1;
for &d in &dependents[i] {
indegree[d] -= 1;
if indegree[d] == 0 {
queue.push_back(d);
}
}
}
visited != n
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
#[test]
fn empty_graph_runs_without_blocking() {
Scheduler::new(2).run(JobGraph::new());
}
#[test]
fn single_job_runs() {
let ran = Arc::new(AtomicUsize::new(0));
let mut graph = JobGraph::new();
let ran2 = ran.clone();
graph.add_job("only", &[], move || {
ran2.fetch_add(1, Ordering::SeqCst);
});
Scheduler::new(1).run(graph);
assert_eq!(ran.load(Ordering::SeqCst), 1);
}
#[test]
fn dependent_job_runs_after_its_dependency() {
let order = Arc::new(StdMutex::new(Vec::new()));
let mut graph = JobGraph::new();
let order_a = order.clone();
let a = graph.add_job("a", &[], move || order_a.lock().unwrap().push("a"));
let order_b = order.clone();
graph.add_job("b", &[a], move || order_b.lock().unwrap().push("b"));
Scheduler::new(4).run(graph);
assert_eq!(*order.lock().unwrap(), vec!["a", "b"]);
}
#[test]
fn independent_jobs_all_run_regardless_of_worker_count() {
let count = Arc::new(AtomicUsize::new(0));
let mut graph = JobGraph::new();
for _ in 0..20 {
let c = count.clone();
graph.add_job("independent", &[], move || {
c.fetch_add(1, Ordering::SeqCst);
});
}
Scheduler::new(4).run(graph);
assert_eq!(count.load(Ordering::SeqCst), 20);
}
#[test]
fn diamond_dependency_runs_in_valid_order() {
let order = Arc::new(StdMutex::new(Vec::new()));
let mut graph = JobGraph::new();
let oa = order.clone();
let a = graph.add_job("a", &[], move || oa.lock().unwrap().push("a"));
let ob = order.clone();
let b = graph.add_job("b", &[a], move || ob.lock().unwrap().push("b"));
let oc = order.clone();
let c = graph.add_job("c", &[a], move || oc.lock().unwrap().push("c"));
let od = order.clone();
graph.add_job("d", &[b, c], move || od.lock().unwrap().push("d"));
Scheduler::new(4).run(graph);
let order = order.lock().unwrap();
assert_eq!(order.first(), Some(&"a"));
assert_eq!(order.last(), Some(&"d"));
assert_eq!(order.len(), 4);
}
#[test]
#[should_panic(expected = "dependency cycle")]
fn cyclic_graph_panics_instead_of_hanging() {
let mut graph = JobGraph::new();
let a = graph.add_job("a", &[], || {});
let b = graph.add_job("b", &[a], || {});
graph.jobs[a.0].dependencies.push(b);
Scheduler::new(2).run(graph);
}
}