meridian_task_core/
lib.rs1use std::collections::VecDeque;
13use std::sync::{Condvar, Mutex};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct JobId(usize);
19
20type Action = Box<dyn FnOnce() + Send>;
21
22struct JobEntry {
23 name: &'static str,
24 action: Option<Action>,
25 dependencies: Vec<JobId>,
26}
27
28#[derive(Default)]
31pub struct JobGraph {
32 jobs: Vec<JobEntry>,
33}
34
35impl JobGraph {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn len(&self) -> usize {
41 self.jobs.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.jobs.is_empty()
46 }
47
48 pub fn job_name(&self, id: JobId) -> &'static str {
51 self.jobs[id.0].name
52 }
53
54 pub fn add_job(
58 &mut self,
59 name: &'static str,
60 dependencies: &[JobId],
61 action: impl FnOnce() + Send + 'static,
62 ) -> JobId {
63 let id = JobId(self.jobs.len());
64 self.jobs.push(JobEntry {
65 name,
66 action: Some(Box::new(action)),
67 dependencies: dependencies.to_vec(),
68 });
69 id
70 }
71}
72
73struct SchedulerState {
77 indegree: Vec<usize>,
78 dependents: Vec<Vec<usize>>,
79 ready: VecDeque<usize>,
80 remaining: usize,
81}
82
83#[derive(Debug)]
97pub struct Scheduler {
98 pub worker_count: usize,
99}
100
101impl Scheduler {
102 pub fn new(worker_count: usize) -> Self {
103 Self {
104 worker_count: worker_count.max(1),
105 }
106 }
107
108 pub fn run(&self, mut graph: JobGraph) {
119 let n = graph.jobs.len();
120 if n == 0 {
121 return;
122 }
123
124 let mut indegree = vec![0usize; n];
125 let mut dependents: Vec<Vec<usize>> = (0..n).map(|_| Vec::new()).collect();
126 for (i, job) in graph.jobs.iter().enumerate() {
127 for dep in &job.dependencies {
128 assert!(
129 dep.0 < n,
130 "Scheduler::run: JobId({}) doesn't belong to this JobGraph",
131 dep.0
132 );
133 indegree[i] += 1;
134 dependents[dep.0].push(i);
135 }
136 }
137 assert!(
138 !has_cycle(&indegree, &dependents, n),
139 "Scheduler::run: dependency cycle in JobGraph"
140 );
141
142 let actions: Mutex<Vec<Option<Action>>> =
143 Mutex::new(graph.jobs.iter_mut().map(|j| j.action.take()).collect());
144
145 let ready: VecDeque<usize> = (0..n).filter(|&i| indegree[i] == 0).collect();
146 let state = Mutex::new(SchedulerState {
147 indegree,
148 dependents,
149 ready,
150 remaining: n,
151 });
152 let cvar = Condvar::new();
153
154 std::thread::scope(|scope| {
155 for _ in 0..self.worker_count {
156 scope.spawn(|| worker_loop(&state, &cvar, &actions));
157 }
158 });
159 }
160}
161
162fn worker_loop(
163 state: &Mutex<SchedulerState>,
164 cvar: &Condvar,
165 actions: &Mutex<Vec<Option<Action>>>,
166) {
167 loop {
168 let job_index = {
169 let mut guard = state.lock().unwrap();
170 loop {
171 if let Some(i) = guard.ready.pop_front() {
172 break Some(i);
173 }
174 if guard.remaining == 0 {
175 break None;
176 }
177 guard = cvar.wait(guard).unwrap();
178 }
179 };
180
181 let Some(i) = job_index else {
182 break;
183 };
184
185 let action = actions.lock().unwrap()[i].take();
186 if let Some(action) = action {
187 action();
188 }
189
190 let mut guard = state.lock().unwrap();
191 let newly_ready: Vec<usize> = std::mem::take(&mut guard.dependents[i])
192 .into_iter()
193 .filter(|&d| {
194 guard.indegree[d] -= 1;
195 guard.indegree[d] == 0
196 })
197 .collect();
198 guard.ready.extend(newly_ready);
199 guard.remaining -= 1;
200 drop(guard);
201 cvar.notify_all();
202 }
203}
204
205fn has_cycle(indegree: &[usize], dependents: &[Vec<usize>], n: usize) -> bool {
208 let mut indegree = indegree.to_vec();
209 let mut queue: VecDeque<usize> = (0..n).filter(|&i| indegree[i] == 0).collect();
210 let mut visited = 0usize;
211 while let Some(i) = queue.pop_front() {
212 visited += 1;
213 for &d in &dependents[i] {
214 indegree[d] -= 1;
215 if indegree[d] == 0 {
216 queue.push_back(d);
217 }
218 }
219 }
220 visited != n
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use std::sync::atomic::{AtomicUsize, Ordering};
227 use std::sync::{Arc, Mutex as StdMutex};
228
229 #[test]
230 fn empty_graph_runs_without_blocking() {
231 Scheduler::new(2).run(JobGraph::new());
232 }
233
234 #[test]
235 fn single_job_runs() {
236 let ran = Arc::new(AtomicUsize::new(0));
237 let mut graph = JobGraph::new();
238 let ran2 = ran.clone();
239 graph.add_job("only", &[], move || {
240 ran2.fetch_add(1, Ordering::SeqCst);
241 });
242 Scheduler::new(1).run(graph);
243 assert_eq!(ran.load(Ordering::SeqCst), 1);
244 }
245
246 #[test]
247 fn dependent_job_runs_after_its_dependency() {
248 let order = Arc::new(StdMutex::new(Vec::new()));
249 let mut graph = JobGraph::new();
250
251 let order_a = order.clone();
252 let a = graph.add_job("a", &[], move || order_a.lock().unwrap().push("a"));
253
254 let order_b = order.clone();
255 graph.add_job("b", &[a], move || order_b.lock().unwrap().push("b"));
256
257 Scheduler::new(4).run(graph);
258 assert_eq!(*order.lock().unwrap(), vec!["a", "b"]);
259 }
260
261 #[test]
262 fn independent_jobs_all_run_regardless_of_worker_count() {
263 let count = Arc::new(AtomicUsize::new(0));
264 let mut graph = JobGraph::new();
265 for _ in 0..20 {
266 let c = count.clone();
267 graph.add_job("independent", &[], move || {
268 c.fetch_add(1, Ordering::SeqCst);
269 });
270 }
271 Scheduler::new(4).run(graph);
272 assert_eq!(count.load(Ordering::SeqCst), 20);
273 }
274
275 #[test]
276 fn diamond_dependency_runs_in_valid_order() {
277 let order = Arc::new(StdMutex::new(Vec::new()));
279 let mut graph = JobGraph::new();
280
281 let oa = order.clone();
282 let a = graph.add_job("a", &[], move || oa.lock().unwrap().push("a"));
283
284 let ob = order.clone();
285 let b = graph.add_job("b", &[a], move || ob.lock().unwrap().push("b"));
286
287 let oc = order.clone();
288 let c = graph.add_job("c", &[a], move || oc.lock().unwrap().push("c"));
289
290 let od = order.clone();
291 graph.add_job("d", &[b, c], move || od.lock().unwrap().push("d"));
292
293 Scheduler::new(4).run(graph);
294
295 let order = order.lock().unwrap();
296 assert_eq!(order.first(), Some(&"a"));
297 assert_eq!(order.last(), Some(&"d"));
298 assert_eq!(order.len(), 4);
299 }
300
301 #[test]
302 #[should_panic(expected = "dependency cycle")]
303 fn cyclic_graph_panics_instead_of_hanging() {
304 let mut graph = JobGraph::new();
312 let a = graph.add_job("a", &[], || {});
313 let b = graph.add_job("b", &[a], || {});
314 graph.jobs[a.0].dependencies.push(b);
315
316 Scheduler::new(2).run(graph);
317 }
318}