use std::future::{Future, IntoFuture};
use std::marker::PhantomData;
use std::pin::Pin;
use std::time::Duration;
use crate::{RunId, RunOutcome, RunStatus, RunTermination, StepErrorKind, TerminalStatus};
use thiserror::Error;
use crate::jobs::job::Job;
use crate::jobs::runner::JobRuntime;
use crate::{Error, Result};
#[derive(Debug, Clone, Error)]
#[error("job failed ({kind:?}): {message}")]
pub struct JobError {
pub kind: StepErrorKind,
pub message: String,
}
#[derive(Debug, Error)]
pub enum JoinError {
#[error(transparent)]
Infra(#[from] Error),
#[error(transparent)]
Job(#[from] JobError),
}
pub struct JobHandle<J: Job> {
id: RunId,
runtime: JobRuntime,
newly_submitted: bool,
_marker: PhantomData<fn() -> J>,
}
impl<J: Job> Clone for JobHandle<J> {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
runtime: self.runtime.clone(),
newly_submitted: self.newly_submitted,
_marker: PhantomData,
}
}
}
impl<J: Job> JobHandle<J> {
pub(crate) fn new(id: RunId, runtime: JobRuntime, newly_submitted: bool) -> Self {
Self {
id,
runtime,
newly_submitted,
_marker: PhantomData,
}
}
pub fn id(&self) -> &RunId {
&self.id
}
pub fn newly_submitted(&self) -> bool {
self.newly_submitted
}
pub async fn status(&self) -> Result<Option<RunStatus>> {
self.runtime.status(&self.id).await
}
pub async fn fetch_result(&self) -> Result<Option<std::result::Result<J::Output, JobError>>> {
match self
.runtime
.inner
.core
.view
.recorded_result(&self.id)
.await?
{
None => Ok(None),
Some(result) => decode_end::<J>(result.termination, Some(result.outcome)).map(Some),
}
}
pub async fn join(&self) -> Result<std::result::Result<J::Output, JobError>> {
let end = self.runtime.wait(&self.id).await?;
decode_end::<J>(end.termination, end.outcome)
}
pub async fn join_timeout(
&self,
timeout: Duration,
) -> Result<Option<std::result::Result<J::Output, JobError>>> {
match self.runtime.wait_timeout(&self.id, timeout).await? {
None => Ok(None),
Some(end) => decode_end::<J>(end.termination, end.outcome).map(Some),
}
}
}
pub(crate) fn decode_end<J: Job>(
termination: RunTermination,
outcome: Option<RunOutcome>,
) -> Result<std::result::Result<J::Output, JobError>> {
if let Some(outcome) = outcome
&& outcome.status == TerminalStatus::Succeeded
{
let output = outcome.result.unwrap_or_default();
return Ok(Ok(rmp_serde::from_slice(&output)?));
}
let message = termination.error.unwrap_or_else(|| {
match termination.status {
TerminalStatus::Cancelled => "job cancelled",
_ => "job terminated without recording an outcome",
}
.to_string()
});
Ok(Err(JobError {
kind: termination.error_kind.unwrap_or(StepErrorKind::Transient),
message,
}))
}
impl<J: Job> IntoFuture for JobHandle<J> {
type Output = std::result::Result<J::Output, JoinError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
match self.join().await {
Ok(Ok(output)) => Ok(output),
Ok(Err(job_error)) => Err(JoinError::Job(job_error)),
Err(infra) => Err(JoinError::Infra(infra)),
}
})
}
}