use std::process::ExitStatus;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use anyhow::Result;
use crankshaft_config::backend::Defaults;
use crankshaft_config::backend::generic::Config;
use futures::FutureExt;
use futures::future::BoxFuture;
use nonempty::NonEmpty;
use regex::Regex;
use tokio::select;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use super::TaskRunError;
use crate::Task;
use crate::service::runner::backend::generic::driver::Driver;
use crate::task::Resources;
pub mod driver;
pub const DEFAULT_MONITOR_FREQUENCY: u64 = 5;
#[derive(Debug)]
pub struct Backend {
driver: Arc<Driver>,
config: Config,
defaults: Option<Defaults>,
}
impl Backend {
pub async fn initialize(config: Config, defaults: Option<Defaults>) -> Result<Self> {
let driver = Driver::initialize(config.driver().clone())
.await
.map(Arc::new)?;
Ok(Self {
driver,
config,
defaults,
})
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn driver(&self) -> &Driver {
&self.driver
}
fn resolve_resources(&self, task: Option<&Resources>) -> Option<Resources> {
let mut resources: Option<Resources> = None;
if let Some(defaults) = &self.defaults {
let defaults = Resources::from(defaults);
resources = Some(resources.unwrap_or_default().apply(&defaults));
}
if let Some(task) = task {
resources = Some(resources.unwrap_or_default().apply(task));
}
resources
}
}
impl crate::Backend for Backend {
fn default_name(&self) -> &'static str {
"generic"
}
fn run(
&self,
task: Task,
mut started: Option<oneshot::Sender<()>>,
token: CancellationToken,
) -> Result<BoxFuture<'static, Result<NonEmpty<ExitStatus>, TaskRunError>>> {
let driver = self.driver.clone();
let config = self.config.clone();
let default_substitutions = self
.resolve_resources(task.resources.as_ref())
.map(|resources| resources.to_hashmap())
.unwrap_or_default();
Ok(async move {
let mut statuses = Vec::new();
let job_id_regex = config
.job_id_regex()
.as_ref()
.map(|pattern| {
Regex::new(pattern)
.with_context(|| format!("job regex `{pattern}` is not valid"))
})
.transpose()?;
for execution in task.executions {
if token.is_cancelled() {
return Err(TaskRunError::Canceled);
}
warn!(
"generic backends do not support images; as such, the directive to use a `{}` \
image will be ignored",
execution.image
);
let mut substitutions = default_substitutions.clone();
if substitutions
.insert(
"command".into(),
shlex::try_join(
std::iter::once(execution.program.as_str())
.chain(execution.args.iter().map(String::as_str)),
)
.map_err(|e| TaskRunError::Other(e.into()))?
.into(),
)
.is_some()
{
unreachable!("the `command` key should not be present here");
};
if let Some(cwd) = execution.work_dir {
if substitutions.insert("cwd".into(), cwd.into()).is_some() {
unreachable!("the `cwd` key should not be present here");
};
}
let submit = config
.resolve_submit(&substitutions)
.context("failed to resolve submit command")?;
let output = driver
.run(submit)
.await
.context("failed to run submit command")?;
if let Some(started) = started.take() {
started.send(()).ok();
}
match job_id_regex {
Some(ref regex) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let captures = regex.captures_iter(&stdout).next().unwrap_or_else(|| {
panic!(
"could not match the job id regex within stdout: `{}`",
stdout
)
});
let id = captures.get(1).map(|c| c.as_str()).unwrap();
substitutions.insert("job_id".into(), id.into());
loop {
let monitor = config
.resolve_monitor(&substitutions)
.context("failed to resolve monitor command")?;
let result = select! {
biased;
_ = token.cancelled() => {
Err(TaskRunError::Canceled)
}
res = driver.run(monitor) => {
res.map_err(TaskRunError::Other)
}
};
if token.is_cancelled() {
let kill = config
.resolve_kill(&substitutions)
.context("failed to resolve kill command")?;
driver
.run(kill)
.await
.context("failed to run kill command")?;
}
let output = result?;
if !output.status.success() {
statuses.push(output.status);
break;
}
tokio::time::sleep(Duration::from_secs(
config
.monitor_frequency()
.unwrap_or(DEFAULT_MONITOR_FREQUENCY),
))
.await;
}
}
_ => {
statuses.push(output.status);
}
}
}
Ok(NonEmpty::from_vec(statuses).unwrap())
}
.boxed())
}
}