use std::marker::PhantomData;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use crate::checkpoint::{PendingStep, StepPlacement, StepPosition, StepWriter};
use crate::connection::Connection;
use crate::error::EngineOnly;
use crate::error::{DurableError, EncodedError, Error, Failure, Result};
use crate::rt::JoinHandle;
use crate::serialization::{EncodedOutput, PayloadEncoding, Serializer, decode_tagged, encode};
use crate::sysdb::types::{Outcome, StepRecord, StepTiming, Timestamp, WorkflowStatus, step_names};
pub struct WorkflowHandle<R, E = crate::EngineOnly> {
conn: Arc<Connection>,
workflow_id: String,
provenance: Provenance,
types: PhantomData<fn() -> (R, E)>,
}
enum Provenance {
Local(JoinHandle<std::result::Result<EncodedOutput, Failure>>),
Polling {
fail_if_missing: bool,
},
}
impl<R, E> WorkflowHandle<R, E> {
pub(crate) fn local(
conn: Arc<Connection>,
workflow_id: String,
task: JoinHandle<std::result::Result<EncodedOutput, Failure>>,
) -> Self {
Self {
conn,
workflow_id,
provenance: Provenance::Local(task),
types: PhantomData,
}
}
pub(crate) fn polling(
conn: Arc<Connection>,
workflow_id: String,
fail_if_missing: bool,
) -> Self {
Self {
conn,
workflow_id,
provenance: Provenance::Polling { fail_if_missing },
types: PhantomData,
}
}
pub fn workflow_id(&self) -> &str {
&self.workflow_id
}
pub async fn status(&self) -> Result<WorkflowStatus> {
let row = self
.conn
.sysdb()
.get_workflow(&self.workflow_id)
.await
.map_err(Error::SystemDatabase)?
.ok_or_else(|| Error::WorkflowNotFound {
workflow_id: self.workflow_id.clone(),
})?;
Ok(row.status)
}
}
impl<R, E> WorkflowHandle<R, E>
where
R: DeserializeOwned,
E: DurableError,
{
pub fn result<'a>(self) -> PendingStep<'a, R, E>
where
R: 'a,
{
let built = ChildResultPlacement::of(&self.conn).map(|awaiting| {
let placement = awaiting.placement().clone();
(awaiting, placement)
});
PendingStep::placed(step_names::GET_RESULT, built, move |awaiting, _| {
self.settle(awaiting)
})
}
pub(crate) async fn settle(self, awaiting: ChildResultPlacement) -> Result<R, E> {
if let Some(recorded) = awaiting
.check(&self.conn, &self.workflow_id)
.await
.map_err(Error::lift)?
{
tracing::debug!(
workflow_id = self.workflow_id,
"the child's outcome was already recorded; the parent does not wait again"
);
return Self::interpret(recorded, self.conn.serializer(), &self.workflow_id);
}
let started_at = Timestamp::now();
let outcome = match self.provenance {
Provenance::Local(task) => match task.await {
Ok(outcome) => outcome,
Err(join) if join.is_cancelled() => Err(Failure::Control(Error::Interrupted {
workflow_id: self.workflow_id.clone(),
})),
Err(join) => std::panic::resume_unwind(join.into_panic()),
},
Provenance::Polling { fail_if_missing } => {
self.conn.adopt(&self.workflow_id, fail_if_missing).await
}
};
awaiting
.record(&self.conn, &self.workflow_id, &outcome, started_at)
.await
.map_err(Error::lift)?;
match outcome {
Ok(output) => output.decode(),
Err(Failure::Recorded(recorded)) => Err(recorded.decode(&self.workflow_id)),
Err(Failure::Control(Error::WorkflowCancelled { workflow_id }))
if awaiting.placement().inside_a_workflow() =>
{
Err(Error::AwaitedWorkflowCancelled { workflow_id })
}
Err(Failure::Control(control)) => Err(control.lift()),
}
}
fn interpret(recorded: StepRecord, configured: &Serializer, workflow_id: &str) -> Result<R, E> {
let tag = recorded.serialization.as_deref();
match recorded.error {
None => decode_tagged(recorded.output.as_deref(), tag, configured, "result"),
Some(encoded) => Err(EncodedError {
encoded,
encoding: PayloadEncoding::of(tag, configured)?,
}
.decode(workflow_id)),
}
}
}
pub(crate) struct ChildResultPlacement(StepPlacement);
impl ChildResultPlacement {
pub(crate) fn of(conn: &Arc<Connection>) -> std::result::Result<Self, Error> {
StepPlacement::of(conn, "awaiting a workflow's result").map(Self)
}
pub(crate) fn placement(&self) -> &StepPlacement {
&self.0
}
async fn check(
&self,
conn: &Connection,
awaited_workflow_id: &str,
) -> std::result::Result<Option<StepRecord>, Error> {
let Some(StepPosition {
workflow_id,
step_id,
}) = self.0.step_position()
else {
return Ok(None);
};
let Some(recorded) = conn
.sysdb()
.check_child_result(workflow_id, step_id)
.await
.map_err(Error::from_execution)?
else {
return Ok(None);
};
if recorded.child_workflow_id.as_deref() != Some(awaited_workflow_id) {
return Err(Error::SystemDatabase(crate::sysdb::Error::UnexpectedStep {
workflow_id: workflow_id.to_owned(),
step_id,
expected: format!("an await of {awaited_workflow_id}"),
recorded: match &recorded.child_workflow_id {
Some(other) => format!("an await of {other}"),
None => "an await of no workflow at all".to_owned(),
},
}));
}
Ok(Some(recorded))
}
async fn record(
&self,
conn: &Connection,
child_workflow_id: &str,
settled: &std::result::Result<EncodedOutput, Failure>,
started_at: Timestamp,
) -> std::result::Result<(), Error> {
let Some(StepWriter {
workflow_id,
step_id,
owner_xid,
}) = self.0.step_writer()
else {
return Ok(());
};
let cancelled;
let configured;
let (outcome, encoding) = match settled {
Ok(output) => (Outcome::Output(output.encoded.as_deref()), &output.encoding),
Err(Failure::Recorded(recorded)) => {
(Outcome::Error(&recorded.encoded), &recorded.encoding)
}
Err(Failure::Control(Error::WorkflowCancelled { workflow_id })) => {
configured = conn.serializer().encoding();
cancelled = encode(
&Error::<EngineOnly>::AwaitedWorkflowCancelled {
workflow_id: workflow_id.clone(),
},
&configured,
"error",
)?;
(Outcome::Error(&cancelled), &configured)
}
Err(Failure::Control(_)) => return Ok(()),
};
conn.sysdb()
.record_child_result(
workflow_id,
step_id,
child_workflow_id,
outcome,
Some(encoding.name()),
Some(StepTiming {
started_at,
completed_at: Timestamp::now(),
}),
owner_xid,
)
.await
.map_err(Error::from_execution)
}
}
impl<R, E> std::fmt::Debug for WorkflowHandle<R, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkflowHandle")
.field("workflow_id", &self.workflow_id)
.field(
"provenance",
match &self.provenance {
Provenance::Local(_) => &"local",
Provenance::Polling { .. } => &"polling",
},
)
.finish_non_exhaustive()
}
}