use std::env;
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::thread;
use std::time::Duration;
use crate::configuration::{ReplicateFailurePolicy, ReplicateScheduling, ReplicateSettings};
use crate::rng_record::ReplicateSeedDeriver;
use super::{ExecutionScope, ExecutionScopeError};
const REPLICATE_INDEX_ENVIRONMENT_VARIABLE: &str = "SCIENTIFIC_WORKFLOW_REPLICATE_INDEX";
const PARALLEL_POLL_INTERVAL: Duration = Duration::from_millis(10);
#[derive(Clone, Debug)]
pub struct ReplicateExecutor {
settings: ReplicateSettings,
output_root: PathBuf,
}
impl ReplicateExecutor {
pub fn new(settings: ReplicateSettings, output_root: impl Into<PathBuf>) -> Self {
Self {
settings,
output_root: output_root.into(),
}
}
pub fn dispatch_current_executable(
&self,
) -> Result<Option<ReplicateContext>, ReplicateExecutionError> {
if let Some(raw_index) = env::var_os(REPLICATE_INDEX_ENVIRONMENT_VARIABLE) {
return self.enter_worker(raw_index).map(Some);
}
let executable = env::current_exe().map_err(ReplicateExecutionError::CurrentExecutable)?;
let arguments = env::args_os().skip(1).collect::<Vec<_>>();
self.prepare_output_scopes()?;
match self.settings.scheduling() {
ReplicateScheduling::Sequential => {
self.run_sequential(&executable, &arguments)?;
}
ReplicateScheduling::Parallel => {
self.run_parallel(&executable, &arguments)?;
}
}
Ok(None)
}
fn enter_worker(
&self,
raw_index: OsString,
) -> Result<ReplicateContext, ReplicateExecutionError> {
let display = raw_index.to_string_lossy().into_owned();
let index =
display
.parse::<u64>()
.map_err(|_| ReplicateExecutionError::InvalidWorkerIndex {
variable: REPLICATE_INDEX_ENVIRONMENT_VARIABLE,
value: display,
})?;
if index >= self.settings.replicates() {
return Err(ReplicateExecutionError::WorkerIndexOutOfRange {
index,
replicates: self.settings.replicates(),
});
}
let directory = self.output_root.join(replicate_directory_name(index));
let execution_scope = ExecutionScope::open_existing(directory)
.map_err(|source| ReplicateExecutionError::PrepareOutput { index, source })?;
Ok(ReplicateContext {
index,
count: self.settings.replicates(),
execution_scope,
seed_deriver: ReplicateSeedDeriver::new(self.settings.base_seed(), index),
})
}
fn run_sequential(
&self,
executable: &Path,
arguments: &[OsString],
) -> Result<(), ReplicateExecutionError> {
let mut failures = Vec::new();
for index in 0..self.settings.replicates() {
let status = self
.child_command(executable, arguments, index)
.status()
.map_err(|source| ReplicateExecutionError::RunProcess { index, source })?;
if !status.success() {
failures.push(index);
if self.settings.failure_policy() == ReplicateFailurePolicy::FailFast {
break;
}
}
}
finish_batch(failures)
}
fn run_parallel(
&self,
executable: &Path,
arguments: &[OsString],
) -> Result<(), ReplicateExecutionError> {
let mut active = Vec::new();
for index in 0..self.settings.replicates() {
let child = match self.child_command(executable, arguments, index).spawn() {
Ok(child) => child,
Err(source) => {
terminate_children(&mut active);
return Err(ReplicateExecutionError::RunProcess { index, source });
}
};
active.push(ActiveReplicate { index, child });
}
let mut failures = Vec::new();
while !active.is_empty() {
let mut position = 0;
let mut completed_any = false;
while position < active.len() {
let status = match active[position].child.try_wait() {
Ok(status) => status,
Err(source) => {
let index = active[position].index;
terminate_children(&mut active);
return Err(ReplicateExecutionError::RunProcess { index, source });
}
};
let Some(status) = status else {
position += 1;
continue;
};
completed_any = true;
let completed = active.swap_remove(position);
if !status.success() {
failures.push(completed.index);
if self.settings.failure_policy() == ReplicateFailurePolicy::FailFast {
terminate_children(&mut active);
failures.sort_unstable();
return finish_batch(failures);
}
}
}
if !completed_any && !active.is_empty() {
thread::sleep(PARALLEL_POLL_INTERVAL);
}
}
failures.sort_unstable();
finish_batch(failures)
}
fn prepare_output_scopes(&self) -> Result<(), ReplicateExecutionError> {
let mut created = Vec::new();
for index in 0..self.settings.replicates() {
match ExecutionScope::create_named(&self.output_root, replicate_directory_name(index)) {
Ok(scope) => created.push(scope),
Err(source) => {
for scope in created.into_iter().rev() {
let _ = std::fs::remove_dir(scope.directory());
}
return Err(ReplicateExecutionError::PrepareOutput { index, source });
}
}
}
Ok(())
}
fn child_command(&self, executable: &Path, arguments: &[OsString], index: u64) -> Command {
let mut command = Command::new(executable);
command
.args(arguments)
.env(REPLICATE_INDEX_ENVIRONMENT_VARIABLE, index.to_string());
command
}
}
#[derive(Clone, Debug)]
pub struct ReplicateContext {
index: u64,
count: u64,
execution_scope: ExecutionScope,
seed_deriver: ReplicateSeedDeriver,
}
impl ReplicateContext {
pub const fn index(&self) -> u64 {
self.index
}
pub const fn count(&self) -> u64 {
self.count
}
pub const fn execution_scope(&self) -> &ExecutionScope {
&self.execution_scope
}
pub fn output_directory(&self) -> &Path {
self.execution_scope.directory()
}
pub const fn seed_deriver(&self) -> ReplicateSeedDeriver {
self.seed_deriver
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReplicateExecutionError {
#[error("failed to resolve the current executable for replicate dispatch")]
CurrentExecutable(#[source] io::Error),
#[error("environment variable `{variable}` contains invalid replicate index `{value}`")]
InvalidWorkerIndex {
variable: &'static str,
value: String,
},
#[error("replicate worker index {index} is outside declared count {replicates}")]
WorkerIndexOutOfRange {
index: u64,
replicates: u64,
},
#[error("failed to prepare output for replicate {index}")]
PrepareOutput {
index: u64,
#[source]
source: ExecutionScopeError,
},
#[error("failed to run subprocess for replicate {index}")]
RunProcess {
index: u64,
#[source]
source: io::Error,
},
#[error("replicate subprocesses failed at indices {indices:?}")]
ReplicatesFailed {
indices: Vec<u64>,
},
}
struct ActiveReplicate {
index: u64,
child: Child,
}
fn replicate_directory_name(index: u64) -> String {
format!("replicate_{index}")
}
fn finish_batch(failures: Vec<u64>) -> Result<(), ReplicateExecutionError> {
if failures.is_empty() {
Ok(())
} else {
Err(ReplicateExecutionError::ReplicatesFailed { indices: failures })
}
}
fn terminate_children(children: &mut Vec<ActiveReplicate>) {
for active in children.iter_mut() {
let _ = active.child.kill();
}
for mut active in children.drain(..) {
let _ = active.child.wait();
}
}