#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt;
use std::process::ExitStatus;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use async_trait::async_trait;
use crankshaft_config::backend::tes::Config;
use crankshaft_events::Event;
use crankshaft_events::next_task_id;
use crankshaft_events::send_event;
use futures::FutureExt as _;
use futures::future::BoxFuture;
use nonempty::NonEmpty;
use tes::v1::Client;
use tes::v1::client::strategy::ExponentialFactorBackoff;
use tes::v1::types::requests::GetTaskParams;
use tes::v1::types::requests::View;
use tes::v1::types::task::State;
use tokio::select;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use tracing::info;
use tracing::trace;
use super::TaskRunError;
use crate::Task;
use crate::service::name::GeneratorIterator;
use crate::service::name::UniqueAlphanumeric;
const DEFAULT_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub struct Backend {
client: Arc<Client>,
interval: Duration,
events: Option<broadcast::Sender<Event>>,
names: Arc<Mutex<GeneratorIterator<UniqueAlphanumeric>>>,
retries: usize,
policy: ExponentialFactorBackoff,
}
impl Backend {
pub fn initialize(
config: Config,
names: Arc<Mutex<GeneratorIterator<UniqueAlphanumeric>>>,
events: Option<broadcast::Sender<Event>>,
) -> Self {
let (url, config, interval) = config.into_parts();
let mut builder = Client::builder().url(url);
if let Some(auth) = &config.auth {
builder = builder.insert_header("Authorization", auth.header_value());
}
Self {
client: Arc::new(builder.try_build().expect("client to build")),
interval: interval
.map(Duration::from_secs)
.unwrap_or(DEFAULT_INTERVAL),
events,
names,
retries: config.retries.unwrap_or_default() as usize,
policy: ExponentialFactorBackoff::from_millis(1000, 2.0)
.max_delay(Duration::from_secs(10)),
}
}
async fn wait_task(
client: &Client,
task_id: u64,
task_name: &str,
tes_id: &str,
interval: Duration,
events: Option<broadcast::Sender<Event>>,
retries: impl Iterator<Item = Duration> + Clone,
) -> Result<NonEmpty<ExitStatus>, TaskRunError> {
info!(
"TES task `{tes_id}` (task `{task_name}`) has been created; waiting for task to start"
);
loop {
let task = client
.get_task(
tes_id,
Some(&GetTaskParams {
view: View::Minimal,
}),
retries.clone(),
)
.await
.context("failed to get task information from TES server")?
.into_minimal()
.unwrap();
trace!("response for TES task `{tes_id}`: {task:?}");
if let Some(ref state) = task.state {
match state {
State::Unknown | State::Queued | State::Initializing => {
trace!(
"TES task `{tes_id}` is not yet running; waiting before polling again"
);
}
State::Running | State::Paused => {
trace!("TES task `{tes_id}` is running; waiting before polling again");
send_event!(events, Event::TaskStarted { id: task_id });
}
State::Canceling => {
trace!("TES task `{tes_id}` is canceling; waiting before polling again");
}
State::SystemError => {
let task = client
.get_task(
tes_id,
Some(&GetTaskParams { view: View::Full }),
retries.clone(),
)
.await
.context("failed to get task information from TES server")?
.into_task()
.unwrap();
let messages = task
.logs
.unwrap_or_default()
.last()
.and_then(|l| l.system_logs.as_ref().map(|l| l.join("\n")))
.unwrap_or_default();
return Err(TaskRunError::Other(anyhow!(
"task failed due to system error:\n\n{messages}"
)));
}
State::Complete | State::ExecutorError => {
let task = client
.get_task(
tes_id,
Some(&GetTaskParams { view: View::Basic }),
retries.clone(),
)
.await
.context("failed to get task information from TES server")?
.into_task()
.unwrap();
if *state == State::Complete {
info!("TES task `{tes_id}` (task `{task_name}`) has completed");
} else {
info!("TES task `{tes_id}` (task `{task_name}`) has failed");
}
let logs = task.logs.unwrap_or_default();
let task = logs.last().context(
"invalid response from TES server: completed task is missing task logs",
)?;
return Ok(NonEmpty::collect(task.logs.iter().map(|executor| {
#[cfg(unix)]
let status = ExitStatus::from_raw(executor.exit_code << 8);
#[cfg(windows)]
let status = ExitStatus::from_raw(executor.exit_code as u32);
status
}))
.context(
"invalid response from TES server: completed task is missing executor \
logs",
)?);
}
State::Canceled => return Err(TaskRunError::Canceled),
State::Preempted => return Err(TaskRunError::Preempted),
}
}
tokio::time::sleep(interval).await;
}
}
}
#[async_trait]
impl crate::Backend for Backend {
fn default_name(&self) -> &'static str {
"tes"
}
fn run(
&self,
task: Task,
token: CancellationToken,
) -> Result<BoxFuture<'static, Result<NonEmpty<ExitStatus>, TaskRunError>>> {
let task_id = next_task_id();
let client = self.client.clone();
let interval = self.interval;
let events = self.events.clone();
let names = self.names.clone();
let retries = self.policy.clone().take(self.retries);
let task_token = CancellationToken::new();
Ok(async move {
let task_name = task.name.clone().unwrap_or_else(|| {
let mut generator = names.lock().unwrap();
generator.next().unwrap()
});
let task = tes::v1::types::requests::Task::try_from(task)?;
let tes_id = client
.create_task(&task, retries.clone())
.await
.context("failed to create task with TES server")?
.id;
send_event!(events, Event::TaskCreated { id: task_id, name: task_name.clone(), tes_id: Some(tes_id.clone()), token: task_token.clone()});
let result = select! {
biased;
_ = task_token.cancelled() =>{
Err(TaskRunError::Canceled)
}
_ = token.cancelled() => {
client
.cancel_task(&tes_id, retries.clone())
.await
.context("failed to cancel task with TES server")?;
Err(TaskRunError::Canceled)
}
res = Self::wait_task(&client, task_id, &task_name, &tes_id, interval, events.clone(), retries.clone()) => {
res
}
};
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())
}
}