modewise_pipeline/
batch.rs1use std::collections::BTreeMap;
2
3use rayon::prelude::*;
4
5use crate::{Artifact, ArtifactStore, PipelineError, Plan, RunResult, run};
6
7pub trait InputSource: Sync {
9 fn input_names(&self) -> Vec<String>;
11 fn recording_ids(&self) -> Vec<String>;
12 fn load(&self, id: &str) -> Result<BTreeMap<String, Artifact>, String>;
13}
14
15pub struct RecordingRun {
17 pub id: String,
18 pub result: Result<RunResult, String>,
19}
20
21pub fn run_batch(
24 plan: &Plan,
25 source: &dyn InputSource,
26 ids: &[String],
27 store: &dyn ArtifactStore,
28 workers: usize,
29 on_done: &(dyn Fn(&RecordingRun) + Sync),
30) -> Result<Vec<RecordingRun>, PipelineError> {
31 let pool = rayon::ThreadPoolBuilder::new()
32 .num_threads(workers)
33 .build()
34 .map_err(|e| PipelineError::Spec(format!("cannot start {workers} workers: {e}")))?;
35 let runs = pool.install(|| {
36 ids.par_iter()
37 .map(|id| {
38 let result = source
39 .load(id)
40 .map_err(|e| format!("loading failed: {e}"))
41 .and_then(|mut inputs| {
42 inputs.retain(|name, _| plan.inputs().contains(name));
44 run(plan, &inputs, store).map_err(|e| e.to_string())
45 });
46 let run = RecordingRun {
47 id: id.clone(),
48 result,
49 };
50 on_done(&run);
51 run
52 })
53 .collect()
54 });
55 Ok(runs)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::PipelineSpec;
62 use crate::test_support::{TestStore, ops, series};
63
64 struct Source;
65
66 impl InputSource for Source {
67 fn input_names(&self) -> Vec<String> {
68 vec!["x".into()]
69 }
70
71 fn recording_ids(&self) -> Vec<String> {
72 vec!["a".into(), "bad".into(), "b".into()]
73 }
74
75 fn load(&self, id: &str) -> Result<BTreeMap<String, Artifact>, String> {
76 match id {
77 "bad" => Err("file missing".into()),
78 _ => Ok(BTreeMap::from([(
79 "x".into(),
80 Artifact::Series(series(&[1.0, id.len() as f64])),
81 )])),
82 }
83 }
84 }
85
86 #[test]
87 fn runs_each_recording_and_keeps_going_after_failures() {
88 let spec = PipelineSpec::from_toml(
89 "[pipeline]\nname = \"b\"\ninputs = [\"x\"]\n[[step]]\nid = \"s\"\nop = \"scale\"\ninput = \"x\"\n",
90 )
91 .unwrap();
92 let ops = ops();
93 let plan = Plan::new(&spec, &ops.registry).unwrap();
94 let ids = Source.recording_ids();
95 let done = std::sync::atomic::AtomicUsize::new(0);
96 let count = |_: &RecordingRun| {
97 done.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
98 };
99 let runs = run_batch(&plan, &Source, &ids, &TestStore::default(), 2, &count).unwrap();
100 assert_eq!(done.into_inner(), 3);
101
102 let outcome: Vec<(&str, bool)> = runs
103 .iter()
104 .map(|r| (r.id.as_str(), r.result.is_ok()))
105 .collect();
106 assert_eq!(outcome, [("a", true), ("bad", false), ("b", true)]);
107 assert!(
108 runs[1]
109 .result
110 .as_ref()
111 .err()
112 .unwrap()
113 .contains("file missing")
114 );
115 }
116}