use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use super::{Arm, Bar, Task, TrialResult, execution, grading, store, trial_results};
use crate::session::limits;
use crate::trial::{Outcome, Output, VoidCause};
use crate::{interrupt, logs, progress};
type Completion<'a> = Box<dyn FnOnce(PathBuf, Output, grading::Score) -> Result<()> + 'a>;
trait Attempt: Sync {
fn run(&self, request: execution::Request<'_>, complete: Completion<'_>) -> Result<()>;
}
impl Attempt for execution::Execution<'_> {
fn run(&self, request: execution::Request<'_>, complete: Completion<'_>) -> Result<()> {
self.run(request, complete)
}
}
pub(super) struct Runner<'a> {
directory: &'a Path,
bar: &'a Bar,
signal: &'a Path,
trace: &'a logs::trace::Context,
execution: Box<dyn Attempt + 'a>,
}
impl<'a> Runner<'a> {
pub fn prepare(
experiment: &'a store::Stored,
signal: &'a Path,
trace: &'a logs::trace::Context,
) -> Result<Self> {
let description = &experiment.description;
let execution = execution::Execution::prepare(
&experiment.directory,
description.prepared.as_ref().context("Arms not prepared")?,
[&description.baseline, &description.candidate],
Duration::from_secs(description.bar.timeout_seconds),
signal,
)?;
Ok(Self {
directory: &experiment.directory,
bar: &description.bar,
signal,
trace,
execution: Box::new(execution),
})
}
pub fn run(&self, task: &Task, arm: Arm, index: usize) -> Result<()> {
let identity = trial_results::Identity {
suite: &task.suite,
task: &task.name,
arm,
trial: index,
};
let mut writer = Writer::resume(self.directory, &identity)?;
if !writer.can_continue(self.bar) {
writer.finish()?;
} else {
loop {
interrupt::check()?;
limits::check(self.signal)?;
self.execution.run(
execution::Request {
task,
identity: trial_results::Identity { ..identity },
attempt: writer.progress.attempts.len(),
trace: self.trace,
},
Box::new(|attempt, output, score| {
let pause = pause_cause(&output);
writer.record_attempt(&attempt, output, score)?;
if !writer.can_continue(self.bar) {
writer.finish()?;
}
if let Some(cause) = pause {
bail!(
"Trial {} paused: {:?}; run the Experiment again to resume",
identity.key(),
cause
);
}
Ok(())
}),
)?;
if !writer.can_continue(self.bar) {
break;
}
}
}
progress::step(format!(
"Trial {} {}",
identity.key(),
writer.progress.outcome
));
Ok(())
}
}
fn pause_cause(output: &Output) -> Option<VoidCause> {
if output.outcome == Outcome::Void {
output.void_cause.filter(|cause| {
matches!(
cause,
VoidCause::UsageLimit | VoidCause::RateLimit | VoidCause::Interrupted
)
})
} else {
None
}
}
struct Writer {
pending: PathBuf,
terminal: PathBuf,
progress: TrialResult,
}
impl Writer {
fn resume(directory: &Path, identity: &trial_results::Identity<'_>) -> Result<Self> {
let key = identity.key();
let trial = directory.join("trials").join(&key);
fs::create_dir_all(&trial)?;
fs::create_dir_all(directory.join("results"))?;
let pending = trial.join("pending.json");
let progress = if pending.exists() {
serde_json::from_slice(&fs::read(&pending)?)?
} else {
TrialResult {
suite: identity.suite.into(),
task: identity.task.into(),
arm: identity.arm,
trial: identity.trial,
outcome: Outcome::Void,
score: None,
attempts: Vec::new(),
}
};
Ok(Self {
pending,
terminal: directory.join("results").join(format!("{key}.json")),
progress,
})
}
fn can_continue(&self, bar: &Bar) -> bool {
self.progress.attempts.last().is_none_or(|latest| {
latest.outcome == Outcome::Void
&& latest
.void_cause
.is_some_and(|cause| bar.void_rules.causes.contains(&cause))
&& self.progress.attempts.len() <= bar.void_rules.retries
})
}
fn record_attempt(
&mut self,
attempt: &Path,
output: Output,
score: grading::Score,
) -> Result<()> {
store::write(&attempt.join("result.json"), &output)?;
self.progress.outcome = output.outcome;
self.progress.score = Some(score);
self.progress.attempts.push(output);
store::write(&self.pending, &self.progress)
}
fn finish(&self) -> Result<()> {
store::write(&self.terminal, &self.progress)
}
}
#[cfg(test)]
mod tests;