use std::process::ExitStatus;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use anyhow::Context as _;
use anyhow::Result;
use crankshaft_config::backend::Defaults;
use crankshaft_config::backend::generic::Config;
use crankshaft_events::Event;
use crankshaft_events::next_task_id;
use crankshaft_events::send_event;
use futures::FutureExt;
use futures::future::BoxFuture;
use nonempty::NonEmpty;
use regex::Regex;
use tokio::select;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use super::TaskRunError;
use crate::Task;
use crate::service::name::GeneratorIterator;
use crate::service::name::UniqueAlphanumeric;
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>,
events: Option<broadcast::Sender<Event>>,
names: Arc<Mutex<GeneratorIterator<UniqueAlphanumeric>>>,
}
impl Backend {
pub async fn initialize(
config: Config,
defaults: Option<Defaults>,
names: Arc<Mutex<GeneratorIterator<UniqueAlphanumeric>>>,
events: Option<broadcast::Sender<Event>>,
) -> Result<Self> {
let driver = Driver::initialize(config.driver().clone())
.await
.map(Arc::new)?;
Ok(Self {
driver,
config,
defaults,
events,
names,
})
}
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,
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();
let task_id = next_task_id();
let events = self.events.clone();
let names = self.names.clone();
let task_token = CancellationToken::new();
Ok(async move {
let task_name = task.name.unwrap_or_else(|| {
let mut generator = names.lock().unwrap();
generator.next().unwrap()
});
let run = async {
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")?;
send_event!(events, Event::TaskStarted { id: task_id });
let output = driver
.run(submit)
.await
.context("failed to run submit command")?;
match &job_id_regex {
Some(regex) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let captures =
regex.captures_iter(&stdout).next().with_context(|| {
format!("failed to match job id regex to 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;
_= task_token.cancelled() => {Err(TaskRunError::Canceled)}
_ = 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;
}
}
None => {
statuses.push(output.status);
}
}
}
Ok(NonEmpty::from_vec(statuses).unwrap())
};
send_event!(
events,
Event::TaskCreated {
id: task_id,
name: task_name.clone(),
tes_id: None,
token: task_token.clone()
}
);
let result = run.await;
match &result {
Ok(statuses) => send_event!(
events,
Event::TaskCompleted {
id: task_id,
exit_statuses: statuses.clone()
}
),
Err(TaskRunError::Canceled) => {
send_event!(events, Event::TaskCanceled { id: task_id })
}
Err(TaskRunError::Preempted) => {
send_event!(events, Event::TaskPreempted { id: task_id })
}
Err(TaskRunError::Other(e)) => send_event!(
events,
Event::TaskFailed {
id: task_id,
message: format!("{e:#}")
}
),
}
result
}
.boxed())
}
}