use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use crate::connection::{Connection, Owner};
use crate::context::Ctx;
use crate::error::{DurableError, Error};
use crate::instance::Executor;
use crate::sysdb::types::{Outcome, StepRecord, StepTiming, Timestamp};
pub(crate) type Built<C> = Result<(C, StepPlacement), Error>;
#[must_use = "a durable call that is not awaited never runs, and if it claimed a step id that id \
is spent; await it, or hand it to a combinator"]
pub struct PendingStep<'a, T, E = crate::EngineOnly> {
name: Arc<str>,
placement: Option<StepPlacement>,
running: Pin<Box<dyn Future<Output = crate::Result<T, E>> + Send + 'a>>,
}
impl<'a, T, E> PendingStep<'a, T, E> {
pub(crate) fn new(
name: Arc<str>,
placement: StepPlacement,
running: impl Future<Output = crate::Result<T, E>> + Send + 'a,
) -> Self {
Self {
name,
placement: Some(placement),
running: Box::pin(running),
}
}
pub(crate) fn placed<C, F, Fut>(name: impl Into<Arc<str>>, built: Built<C>, run: F) -> Self
where
C: Send + 'a,
F: FnOnce(C, StepPlacement) -> Fut + Send + 'a,
Fut: Future<Output = crate::Result<T, E>> + Send + 'a,
T: 'a,
E: 'a,
{
let placement = built.as_ref().ok().map(|(_, placement)| placement.clone());
Self {
name: name.into(),
placement,
running: Box::pin(async move {
let (carried, placement) = built.map_err(Error::lift)?;
run(carried, placement).await
}),
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn step_id(&self) -> Option<i32> {
self.placement.as_ref().and_then(StepPlacement::step_id)
}
}
impl<'a, T, E> PendingStep<'a, T, E> {
#[must_use = "a durable call that is not awaited never runs, and if it claimed a step id that \
id is spent; await it, or hand it to a combinator"]
pub fn map_error<F>(self, convert: impl Fn(E) -> F + Copy + Send + 'a) -> PendingStep<'a, T, F>
where
T: 'a,
E: 'a,
F: 'a,
{
let Self {
name,
placement,
running,
} = self;
PendingStep {
name,
placement,
running: Box::pin(async move {
running
.await
.map_err(|failed| failed.map_application(convert))
}),
}
}
}
impl<'a, T> PendingStep<'a, T, crate::EngineOnly> {
#[must_use = "a durable call that is not awaited never runs, and if it claimed a step id that \
id is spent; await it, or hand it to a combinator"]
pub fn lift<F>(self) -> PendingStep<'a, T, F>
where
T: 'a,
F: 'a,
{
self.map_error(|impossible| match impossible {})
}
}
impl<T, E> Future for PendingStep<'_, T, E> {
type Output = crate::Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
if let Some(placement) = &this.placement
&& let Err(refused) = Ctx::with_current(|here| placement.check_here(&this.name, here))
{
return Poll::Ready(Err(refused));
}
this.running.as_mut().poll(cx)
}
}
impl<T, E> std::fmt::Debug for PendingStep<'_, T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PendingStep")
.field("name", &self.name)
.field("step_id", &self.step_id())
.finish_non_exhaustive()
}
}
pub(crate) enum StepDurability<'a> {
Recorded { ctx: &'a Ctx, step_id: i32 },
Plain,
}
pub(crate) fn revive<E: DurableError>(recorded: &str, step: &str) -> Error<E> {
serde_json::from_str(recorded).unwrap_or_else(|_| Error::StepFailed {
step: step.to_owned(),
message: recorded.to_owned(),
})
}
#[derive(Clone)]
pub(crate) enum StepPlacement {
Outside,
InsideStep { ctx: Ctx },
ClientConnection,
Recorded { ctx: Ctx, step_id: i32 },
}
impl StepPlacement {
pub(crate) fn of(conn: &Arc<Connection>, operation: &'static str) -> Result<Self, Error> {
let Some(ctx) = Ctx::current() else {
return Ok(Self::Outside);
};
if ctx.step_marker().is_some() {
return Ok(Self::InsideStep { ctx });
}
if !Arc::ptr_eq(ctx.executor().connection(), conn) {
return match conn.owner() {
Owner::Client => Ok(Self::ClientConnection),
Owner::Application => Err(Error::WrongInstance {
operation: operation.into(),
}),
};
}
let step_id = ctx.next_step_id();
Ok(Self::Recorded { ctx, step_id })
}
pub(crate) fn taken(
executor: Result<Arc<Executor>, Error>,
operation: &'static str,
) -> Result<(Arc<Executor>, Self), Error> {
let executor = executor?;
let placement = Self::of(executor.connection(), operation)?;
Ok((executor, placement))
}
pub(crate) fn ambient_connection(operation: &'static str) -> Result<Arc<Connection>, Error> {
Ctx::current()
.map(|ctx| Arc::clone(ctx.executor().connection()))
.ok_or(Error::NotInWorkflow {
operation: operation.into(),
})
}
pub(crate) fn here() -> Self {
Ctx::current().map_or(Self::Outside, Self::at)
}
pub(crate) fn at(ctx: Ctx) -> Self {
if ctx.step_marker().is_some() {
return Self::InsideStep { ctx };
}
let step_id = ctx.next_step_id();
Self::Recorded { ctx, step_id }
}
pub(crate) fn next_step_id(&self) -> Option<i32> {
match self {
Self::Recorded { ctx, .. } => Some(ctx.next_step_id()),
Self::Outside | Self::InsideStep { .. } | Self::ClientConnection => None,
}
}
pub(crate) fn step_id(&self) -> Option<i32> {
match self {
Self::Recorded { step_id, .. } => Some(*step_id),
Self::Outside | Self::InsideStep { .. } | Self::ClientConnection => None,
}
}
pub(crate) fn whereabouts(&self) -> std::borrow::Cow<'static, str> {
match self {
Self::Recorded { ctx, .. } | Self::InsideStep { ctx } => Self::describe(Some(ctx)),
Self::ClientConnection => "on a client's connection".into(),
Self::Outside => Self::describe(None),
}
}
pub(crate) fn describe(ctx: Option<&Ctx>) -> std::borrow::Cow<'static, str> {
match ctx {
Some(ctx) if ctx.step_marker().is_some() => {
format!("inside a step of workflow {}", ctx.workflow_id()).into()
}
Some(ctx) => format!("in workflow {}", ctx.workflow_id()).into(),
None => "outside a workflow".into(),
}
}
pub(crate) fn check_here<E>(
&self,
step: &str,
ambient: Option<&Ctx>,
) -> crate::Result<StepDurability<'_>, E> {
match (self, ambient) {
(Self::Recorded { ctx, step_id }, Some(here))
if here.is_same_execution(ctx) && here.step_marker().is_none() =>
{
Ok(StepDurability::Recorded {
ctx,
step_id: *step_id,
})
}
(Self::InsideStep { ctx }, Some(here))
if ctx.step_marker().is_some() && here.step_marker() == ctx.step_marker() =>
{
Ok(StepDurability::Plain)
}
(Self::Outside, None) => Ok(StepDurability::Plain),
(Self::ClientConnection, _) => Ok(StepDurability::Plain),
_ => Err(Error::StepBuiltElsewhere {
step: step.to_owned(),
built: self.whereabouts(),
polled: match ambient {
Some(here)
if here.step_marker().is_some()
&& matches!(self, Self::InsideStep { .. }) =>
{
format!("inside a different step of workflow {}", here.workflow_id()).into()
}
here => Self::describe(here),
},
}),
}
}
pub(crate) fn inside_a_workflow(&self) -> bool {
!matches!(self, Self::Outside)
}
pub(crate) fn step(&self) -> Option<(&str, i32)> {
match self {
Self::Recorded { ctx, step_id } => Some((ctx.workflow_id(), *step_id)),
_ => None,
}
}
pub(crate) fn executor(&self) -> Option<&Arc<Executor>> {
match self {
Self::Recorded { ctx, .. } | Self::InsideStep { ctx } => Some(ctx.executor()),
Self::Outside | Self::ClientConnection => None,
}
}
pub(crate) async fn check(
&self,
conn: &Connection,
step_name: &str,
) -> Result<Option<StepRecord>, Error> {
let Some((workflow_id, step_id)) = self.step() else {
return Ok(None);
};
conn.sysdb()
.check_step(workflow_id, step_id, step_name)
.await
.map_err(Error::SystemDatabase)
}
pub(crate) async fn record(
&self,
conn: &Connection,
step_name: &str,
outcome: Outcome<'_>,
started_at: Timestamp,
) -> Result<(), Error> {
let Some((workflow_id, step_id)) = self.step() else {
return Ok(());
};
conn.sysdb()
.record_step(
workflow_id,
step_id,
step_name,
outcome,
Some(conn.serializer().name()),
Some(StepTiming {
started_at,
completed_at: Timestamp::now(),
}),
)
.await
.map_err(Error::SystemDatabase)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EngineOnly;
use crate::context::StepStatus;
use tokio_util::sync::CancellationToken;
async fn contexts() -> (Ctx, Ctx, Ctx, crate::DBOS, dbos_test_support::TestDatabase) {
let db = dbos_test_support::test_database().await;
let dbos = crate::DBOS::new(crate::Config {
migrate: false,
app_version: Some("1.0.0".to_owned()),
..crate::Config::new("placement-test", db.url())
});
dbos.launch().await.expect("launch failed");
let proper = Ctx::new(dbos.executor("test").expect("launched"), "wf", None);
let body = || {
proper.in_step_scope(CancellationToken::new(), StepStatus::first(0), async {
Ctx::current().expect("inside the scope")
})
};
let in_step = body().await;
let sibling = body().await;
assert_ne!(
in_step.step_marker(),
sibling.step_marker(),
"each entry into a step body is a body of its own"
);
(proper, in_step, sibling, dbos, db)
}
fn durability(outcome: &crate::Result<StepDurability<'_>, EngineOnly>) -> &'static str {
match outcome {
Ok(StepDurability::Recorded { .. }) => "recorded",
Ok(StepDurability::Plain) => "plain",
Err(Error::StepBuiltElsewhere { .. }) => "refused",
Err(other) => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn a_call_is_durable_only_where_its_id_means_something() {
let (proper, in_step, sibling, dbos, _db) = contexts().await;
let other = Ctx::new(dbos.executor("test").expect("launched"), "wf-other", None);
let rerun = Ctx::new(dbos.executor("test").expect("launched"), "wf", None);
let recorded = StepPlacement::Recorded {
ctx: proper.clone(),
step_id: 0,
};
let inside = StepPlacement::InsideStep {
ctx: in_step.clone(),
};
for (built, polled, expected, why) in [
(
&recorded,
Some(&proper),
"recorded",
"built and polled at a step boundary",
),
(
&recorded,
Some(&in_step),
"refused",
"carried into a step body",
),
(&recorded, None, "refused", "carried out of the workflow"),
(
&recorded,
Some(&other),
"refused",
"carried into another workflow",
),
(
&recorded,
Some(&rerun),
"refused",
"carried into a second execution of the same workflow id",
),
(&inside, Some(&in_step), "plain", "the body it was built in"),
(
&inside,
Some(&proper),
"refused",
"escaped to the workflow proper",
),
(&inside, None, "refused", "escaped the workflow"),
(
&inside,
Some(&sibling),
"refused",
"carried into another step's body",
),
(
&StepPlacement::Outside,
None,
"plain",
"no workflow either side",
),
(
&StepPlacement::Outside,
Some(&proper),
"refused",
"built before the workflow",
),
(
&StepPlacement::Outside,
Some(&in_step),
"refused",
"built before the step",
),
(
&StepPlacement::ClientConnection,
None,
"plain",
"a client's, outside",
),
(
&StepPlacement::ClientConnection,
Some(&proper),
"plain",
"a client's, in a workflow",
),
(
&StepPlacement::ClientConnection,
Some(&in_step),
"plain",
"a client's, in a step",
),
] {
assert_eq!(
durability(&built.check_here::<EngineOnly>("call", polled)),
expected,
"{why}: built {}, polled {}",
built.whereabouts(),
StepPlacement::describe(polled)
);
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_refusal_names_where_it_was_built_and_where_it_is_polled() {
let (proper, in_step, sibling, dbos, _db) = contexts().await;
let built_inside = StepPlacement::InsideStep {
ctx: in_step.clone(),
};
let Err(Error::<EngineOnly>::StepBuiltElsewhere {
step,
built,
polled,
}) = built_inside.check_here("call", Some(&proper))
else {
panic!("a call that left its step body is refused");
};
assert_eq!(step, "call");
assert_eq!(built, "inside a step of workflow wf");
assert_eq!(polled, "in workflow wf");
assert_eq!(
built_inside.whereabouts(),
StepPlacement::describe(Some(&in_step)),
"a place should read the same whichever side of the refusal names it",
);
let Err(Error::<EngineOnly>::StepBuiltElsewhere { built, polled, .. }) =
built_inside.check_here("call", Some(&sibling))
else {
panic!("a call carried into another step's body is refused");
};
assert_eq!(built, "inside a step of workflow wf");
assert_eq!(polled, "inside a different step of workflow wf");
dbos.shutdown().await;
}
}