scientific_workflow/execution/
replicate.rs1use std::env;
4use std::ffi::OsString;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::process::{Child, Command};
8use std::thread;
9use std::time::Duration;
10
11use crate::configuration::{ReplicateFailurePolicy, ReplicateScheduling, ReplicateSettings};
12use crate::rng_record::ReplicateSeedDeriver;
13
14use super::{ExecutionScope, ExecutionScopeError};
15
16const REPLICATE_INDEX_ENVIRONMENT_VARIABLE: &str = "SCIENTIFIC_WORKFLOW_REPLICATE_INDEX";
17const PARALLEL_POLL_INTERVAL: Duration = Duration::from_millis(10);
18
19#[derive(Clone, Debug)]
26pub struct ReplicateExecutor {
27 settings: ReplicateSettings,
28 output_root: PathBuf,
29}
30
31impl ReplicateExecutor {
32 pub fn new(settings: ReplicateSettings, output_root: impl Into<PathBuf>) -> Self {
34 Self {
35 settings,
36 output_root: output_root.into(),
37 }
38 }
39
40 pub fn dispatch_current_executable(
47 &self,
48 ) -> Result<Option<ReplicateContext>, ReplicateExecutionError> {
49 if let Some(raw_index) = env::var_os(REPLICATE_INDEX_ENVIRONMENT_VARIABLE) {
50 return self.enter_worker(raw_index).map(Some);
51 }
52
53 let executable = env::current_exe().map_err(ReplicateExecutionError::CurrentExecutable)?;
54 let arguments = env::args_os().skip(1).collect::<Vec<_>>();
55 self.prepare_output_scopes()?;
56 match self.settings.scheduling() {
57 ReplicateScheduling::Sequential => {
58 self.run_sequential(&executable, &arguments)?;
59 }
60 ReplicateScheduling::Parallel => {
61 self.run_parallel(&executable, &arguments)?;
62 }
63 }
64 Ok(None)
65 }
66
67 fn enter_worker(
68 &self,
69 raw_index: OsString,
70 ) -> Result<ReplicateContext, ReplicateExecutionError> {
71 let display = raw_index.to_string_lossy().into_owned();
72 let index =
73 display
74 .parse::<u64>()
75 .map_err(|_| ReplicateExecutionError::InvalidWorkerIndex {
76 variable: REPLICATE_INDEX_ENVIRONMENT_VARIABLE,
77 value: display,
78 })?;
79 if index >= self.settings.replicates() {
80 return Err(ReplicateExecutionError::WorkerIndexOutOfRange {
81 index,
82 replicates: self.settings.replicates(),
83 });
84 }
85 let directory = self.output_root.join(replicate_directory_name(index));
86 let execution_scope = ExecutionScope::open_existing(directory)
87 .map_err(|source| ReplicateExecutionError::PrepareOutput { index, source })?;
88 Ok(ReplicateContext {
89 index,
90 count: self.settings.replicates(),
91 execution_scope,
92 seed_deriver: ReplicateSeedDeriver::new(self.settings.base_seed(), index),
93 })
94 }
95
96 fn run_sequential(
97 &self,
98 executable: &Path,
99 arguments: &[OsString],
100 ) -> Result<(), ReplicateExecutionError> {
101 let mut failures = Vec::new();
102 for index in 0..self.settings.replicates() {
103 let status = self
104 .child_command(executable, arguments, index)
105 .status()
106 .map_err(|source| ReplicateExecutionError::RunProcess { index, source })?;
107 if !status.success() {
108 failures.push(index);
109 if self.settings.failure_policy() == ReplicateFailurePolicy::FailFast {
110 break;
111 }
112 }
113 }
114 finish_batch(failures)
115 }
116
117 fn run_parallel(
118 &self,
119 executable: &Path,
120 arguments: &[OsString],
121 ) -> Result<(), ReplicateExecutionError> {
122 let mut active = Vec::new();
123 for index in 0..self.settings.replicates() {
124 let child = match self.child_command(executable, arguments, index).spawn() {
125 Ok(child) => child,
126 Err(source) => {
127 terminate_children(&mut active);
128 return Err(ReplicateExecutionError::RunProcess { index, source });
129 }
130 };
131 active.push(ActiveReplicate { index, child });
132 }
133
134 let mut failures = Vec::new();
135 while !active.is_empty() {
136 let mut position = 0;
137 let mut completed_any = false;
138 while position < active.len() {
139 let status = match active[position].child.try_wait() {
140 Ok(status) => status,
141 Err(source) => {
142 let index = active[position].index;
143 terminate_children(&mut active);
144 return Err(ReplicateExecutionError::RunProcess { index, source });
145 }
146 };
147 let Some(status) = status else {
148 position += 1;
149 continue;
150 };
151 completed_any = true;
152 let completed = active.swap_remove(position);
153 if !status.success() {
154 failures.push(completed.index);
155 if self.settings.failure_policy() == ReplicateFailurePolicy::FailFast {
156 terminate_children(&mut active);
157 failures.sort_unstable();
158 return finish_batch(failures);
159 }
160 }
161 }
162 if !completed_any && !active.is_empty() {
163 thread::sleep(PARALLEL_POLL_INTERVAL);
164 }
165 }
166 failures.sort_unstable();
167 finish_batch(failures)
168 }
169
170 fn prepare_output_scopes(&self) -> Result<(), ReplicateExecutionError> {
171 let mut created = Vec::new();
172 for index in 0..self.settings.replicates() {
173 match ExecutionScope::create_named(&self.output_root, replicate_directory_name(index)) {
174 Ok(scope) => created.push(scope),
175 Err(source) => {
176 for scope in created.into_iter().rev() {
177 let _ = std::fs::remove_dir(scope.directory());
178 }
179 return Err(ReplicateExecutionError::PrepareOutput { index, source });
180 }
181 }
182 }
183 Ok(())
184 }
185
186 fn child_command(&self, executable: &Path, arguments: &[OsString], index: u64) -> Command {
187 let mut command = Command::new(executable);
188 command
189 .args(arguments)
190 .env(REPLICATE_INDEX_ENVIRONMENT_VARIABLE, index.to_string());
191 command
192 }
193}
194
195#[derive(Clone, Debug)]
197pub struct ReplicateContext {
198 index: u64,
199 count: u64,
200 execution_scope: ExecutionScope,
201 seed_deriver: ReplicateSeedDeriver,
202}
203
204impl ReplicateContext {
205 pub const fn index(&self) -> u64 {
207 self.index
208 }
209
210 pub const fn count(&self) -> u64 {
212 self.count
213 }
214
215 pub const fn execution_scope(&self) -> &ExecutionScope {
217 &self.execution_scope
218 }
219
220 pub fn output_directory(&self) -> &Path {
222 self.execution_scope.directory()
223 }
224
225 pub const fn seed_deriver(&self) -> ReplicateSeedDeriver {
227 self.seed_deriver
228 }
229}
230
231#[derive(Debug, thiserror::Error)]
233#[non_exhaustive]
234pub enum ReplicateExecutionError {
235 #[error("failed to resolve the current executable for replicate dispatch")]
237 CurrentExecutable(#[source] io::Error),
238
239 #[error("environment variable `{variable}` contains invalid replicate index `{value}`")]
241 InvalidWorkerIndex {
242 variable: &'static str,
244 value: String,
246 },
247
248 #[error("replicate worker index {index} is outside declared count {replicates}")]
250 WorkerIndexOutOfRange {
251 index: u64,
253 replicates: u64,
255 },
256
257 #[error("failed to prepare output for replicate {index}")]
259 PrepareOutput {
260 index: u64,
262 #[source]
264 source: ExecutionScopeError,
265 },
266
267 #[error("failed to run subprocess for replicate {index}")]
269 RunProcess {
270 index: u64,
272 #[source]
274 source: io::Error,
275 },
276
277 #[error("replicate subprocesses failed at indices {indices:?}")]
279 ReplicatesFailed {
280 indices: Vec<u64>,
282 },
283}
284
285struct ActiveReplicate {
286 index: u64,
287 child: Child,
288}
289
290fn replicate_directory_name(index: u64) -> String {
291 format!("replicate_{index}")
292}
293
294fn finish_batch(failures: Vec<u64>) -> Result<(), ReplicateExecutionError> {
295 if failures.is_empty() {
296 Ok(())
297 } else {
298 Err(ReplicateExecutionError::ReplicatesFailed { indices: failures })
299 }
300}
301
302fn terminate_children(children: &mut Vec<ActiveReplicate>) {
303 for active in children.iter_mut() {
304 let _ = active.child.kill();
305 }
306 for mut active in children.drain(..) {
307 let _ = active.child.wait();
308 }
309}