use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::task::AbortHandle;
use tracing::Instrument;
use crate::checkpoint::{PendingStep, StepPlacement};
use crate::connection::Connection;
use crate::context::Ctx;
use crate::error::{DurableError, Error, Failure, Result};
use crate::handle::{ChildResultPlacement, WorkflowHandle};
use crate::instance::Executor;
use crate::registry::{WorkflowKey, WorkflowRef};
use crate::serialization::encode;
use crate::sysdb::types::{
AwaitedOutcome, InitWorkflowCaller, NewWorkflow, Outcome, OutcomeWrite, Submission, Timestamp,
WorkflowInitResult,
};
pub(crate) const MAX_RECOVERY_ATTEMPTS: i64 = 100;
#[derive(Default)]
pub(crate) struct Tasks {
state: Mutex<State>,
ended: tokio::sync::Notify,
}
#[derive(Default)]
struct State {
running: Vec<AbortHandle>,
live: usize,
closed: bool,
}
impl Tasks {
fn insert(&self, handle: AbortHandle) {
let mut state = self.lock();
if state.closed {
drop(state);
handle.abort();
return;
}
state.running.retain(|handle| !handle.is_finished());
state.running.push(handle);
}
fn arrived(&self) {
self.lock().live += 1;
}
fn departed(&self) {
{
let mut state = self.lock();
state.live = state.live.saturating_sub(1);
}
self.ended.notify_waiters();
}
pub(crate) async fn abort_all(&self) -> usize {
let cancelled = {
let mut state = self.lock();
state.closed = true;
let live: Vec<AbortHandle> = state
.running
.drain(..)
.filter(|handle| !handle.is_finished())
.collect();
for handle in &live {
handle.abort();
}
live.len()
};
loop {
let ended = self.ended.notified();
tokio::pin!(ended);
ended.as_mut().enable();
let live = self.lock().live;
if live == 0 {
break;
}
ended.await;
}
cancelled
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
struct TaskGuard(Arc<Executor>);
impl Drop for TaskGuard {
fn drop(&mut self) {
self.0.tasks().departed();
}
}
pub(crate) fn spawn_tracked<F>(
executor: &Arc<Executor>,
future: F,
) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
executor.tasks().arrived();
let guard = TaskGuard(Arc::clone(executor));
let task = executor.runtime().clone().spawn(async move {
let _guard = guard;
future.await
});
executor.tasks().insert(task.abort_handle());
task
}
struct PanicLog<'a> {
workflow_id: &'a str,
}
impl Drop for PanicLog<'_> {
fn drop(&mut self) {
if std::thread::panicking() {
tracing::error!(
workflow_id = self.workflow_id,
"the workflow body panicked: no outcome is recorded, and the row stays PENDING \
for a later executor to recover"
);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RunOptions<'a> {
pub workflow_id: Option<&'a str>,
pub timeout: Timeout,
pub attributes: Option<&'a serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default)]
pub struct StartOptions<'a> {
pub workflow_id: Option<&'a str>,
pub timeout: Timeout,
pub queue: Option<Enqueue<'a>>,
pub attributes: Option<&'a serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Enqueue<'a> {
pub name: &'a str,
pub deduplication_id: Option<&'a str>,
pub priority: Option<u32>,
pub partition_key: Option<&'a str>,
pub delay: Option<Duration>,
pub duplication_policy: DuplicationPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DuplicationPolicy {
#[default]
Reject,
ReturnExisting,
}
impl<'a> Enqueue<'a> {
pub fn new(name: &'a str) -> Self {
Self {
name,
deduplication_id: None,
priority: None,
partition_key: None,
delay: None,
duplication_policy: DuplicationPolicy::default(),
}
}
pub(crate) fn validate(&self) -> Result<()> {
let refuse = |message: String| {
Err(Error::Config(format!(
"enqueue onto `{}`: {message}",
self.name
)))
};
if self.deduplication_id.is_some() && self.partition_key.is_some() {
return refuse(
"`deduplication_id` and `partition_key` cannot both be set: a partitioned \
queue's dequeue and a deduplication key enforce different things"
.to_owned(),
);
}
if self.duplication_policy == DuplicationPolicy::ReturnExisting
&& self.deduplication_id.is_none()
{
return refuse(
"`DuplicationPolicy::ReturnExisting` needs a `deduplication_id`: with no key there is \
no collision to resolve"
.to_owned(),
);
}
if let Some(priority) = self.priority {
if priority == 0 {
return refuse(
"`priority` must be at least 1; use `None` for an unprioritised workflow"
.to_owned(),
);
}
if priority > i32::MAX as u32 {
return refuse(format!(
"`priority` must be at most {}, got {priority}",
i32::MAX
));
}
}
Ok(())
}
pub(crate) fn stored_priority(&self) -> i32 {
self.priority.map_or(0, |priority| priority as i32)
}
}
pub(crate) fn new_row<'a>(workflow_id: &'a str, enqueue: Option<&Enqueue<'a>>) -> NewWorkflow<'a> {
NewWorkflow {
queue_name: enqueue.map(|enqueue| enqueue.name),
deduplication_id: enqueue.and_then(|enqueue| enqueue.deduplication_id),
priority: enqueue.map_or(0, Enqueue::stored_priority),
queue_partition_key: enqueue.and_then(|enqueue| enqueue.partition_key),
delay: enqueue.and_then(|enqueue| enqueue.delay),
..NewWorkflow::new(workflow_id)
}
}
struct OwnedStart {
workflow_id: String,
timeout: Timeout,
queue: Option<OwnedEnqueue>,
input: Option<String>,
attributes: Option<String>,
started_at: Timestamp,
}
struct OwnedEnqueue {
name: String,
deduplication_id: Option<String>,
priority: Option<u32>,
partition_key: Option<String>,
delay: Option<Duration>,
duplication_policy: DuplicationPolicy,
}
impl From<&Enqueue<'_>> for OwnedEnqueue {
fn from(enqueue: &Enqueue<'_>) -> Self {
Self {
name: enqueue.name.to_owned(),
deduplication_id: enqueue.deduplication_id.map(str::to_owned),
priority: enqueue.priority,
partition_key: enqueue.partition_key.map(str::to_owned),
delay: enqueue.delay,
duplication_policy: enqueue.duplication_policy,
}
}
}
impl OwnedEnqueue {
fn as_enqueue(&self) -> Enqueue<'_> {
Enqueue {
name: &self.name,
deduplication_id: self.deduplication_id.as_deref(),
priority: self.priority,
partition_key: self.partition_key.as_deref(),
delay: self.delay,
duplication_policy: self.duplication_policy,
}
}
}
pub(crate) enum Submitted {
Created(WorkflowInitResult),
Joined(String),
}
pub(crate) async fn init_or_join(
conn: &Connection,
new: &NewWorkflow<'_>,
policy: DuplicationPolicy,
caller: Option<InitWorkflowCaller<'_>>,
) -> Result<Submitted> {
loop {
match conn
.sysdb()
.init_workflow(new, Some(MAX_RECOVERY_ATTEMPTS), Submission::Fresh, caller)
.await
{
Ok(initialized) => return Ok(Submitted::Created(initialized)),
Err(crate::sysdb::Error::QueueDeduplicated {
queue_name,
deduplication_id,
..
}) if policy == DuplicationPolicy::ReturnExisting => {
match conn
.sysdb()
.get_deduplication_key_holder(&queue_name, &deduplication_id)
.await
.map_err(Error::SystemDatabase)?
{
Some(holder) => {
tracing::debug!(
workflow_id = holder,
deduplication_id,
"the deduplication key is held; the handle joins its holder"
);
return Ok(Submitted::Joined(holder));
}
None => continue,
}
}
Err(error) => return Err(Error::SystemDatabase(error)),
}
}
}
pub(crate) fn encode_attributes(
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Result<Option<String>> {
attributes
.map(serde_json::to_string)
.transpose()
.map_err(|error| Error::Serialization {
what: "attributes".into(),
message: error.to_string(),
source: Some(error),
})
}
impl<'a> From<RunOptions<'a>> for StartOptions<'a> {
fn from(options: RunOptions<'a>) -> Self {
Self {
workflow_id: options.workflow_id,
timeout: options.timeout,
attributes: options.attributes,
queue: None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Timeout {
#[default]
Inherit,
None,
Explicit(Duration),
}
impl From<Duration> for Timeout {
fn from(timeout: Duration) -> Self {
Self::Explicit(timeout)
}
}
impl Timeout {
fn budget(self) -> Option<Duration> {
match self {
Self::Explicit(timeout) => Some(timeout),
Self::Inherit | Self::None => Option::None,
}
}
}
impl<P, R, E> WorkflowRef<P, R, E>
where
P: Serialize + DeserializeOwned + Send + 'static,
R: Serialize + DeserializeOwned + Send + 'static,
E: DurableError,
{
pub fn run(&self, input: P) -> PendingRun<'_, R, E> {
self.run_with(input, RunOptions::default())
}
pub fn run_with<'a>(&'a self, input: P, options: RunOptions<'a>) -> PendingRun<'a, R, E> {
let name: Arc<str> = Arc::from(self.key().name.as_str());
let options: StartOptions<'a> = options.into();
let built = self.place(&options).map(StartPlacement::into_parts);
let awaiting = built
.as_ref()
.ok()
.map(|(executor, _)| ChildResultPlacement::of(executor.connection()));
PendingWorkflow(PendingStep::placed(
name,
built,
move |executor, placement| async move {
let handle = self
.started(StartPlacement::of(executor, placement), input, options)
.await
.map_err(Error::lift)?;
let awaiting = match awaiting {
Some(Ok(awaiting)) => awaiting,
Some(Err(refused)) => return Err(refused.lift()),
None => {
unreachable!("a run that could not be placed never reaches its own body")
}
};
handle.settle(awaiting).await
},
))
}
fn place(&self, options: &StartOptions<'_>) -> Result<StartPlacement> {
let executor = self.dbos().executor("start a workflow")?;
if let Some(enqueue) = options.queue.as_ref() {
enqueue.validate()?;
}
let placement = StepPlacement::of(executor.connection(), "starting a workflow")?;
if matches!(placement, StepPlacement::InsideStep { .. }) {
return Err(Error::InsideStep {
operation: "starting a workflow".into(),
});
}
Ok(StartPlacement {
executor,
placement,
})
}
pub fn start(&self, input: P) -> PendingStart<'_, R, E> {
self.start_with(input, StartOptions::default())
}
pub fn start_with<'a>(&'a self, input: P, options: StartOptions<'a>) -> PendingStart<'a, R, E> {
let name: Arc<str> = Arc::from(self.key().name.as_str());
let built = self.place(&options).map(StartPlacement::into_parts);
PendingStart {
starting: PendingWorkflow(PendingStep::placed(
name,
built,
move |executor, placement| async move {
self.started(StartPlacement::of(executor, placement), input, options)
.await
},
)),
channel: PhantomData,
}
}
async fn started(
&self,
placed: StartPlacement,
input: P,
options: StartOptions<'_>,
) -> Result<WorkflowHandle<R, E>> {
let workflow_id = child_workflow_id(options.workflow_id, placed.parent().as_ref());
let request = OwnedStart {
workflow_id: workflow_id.clone(),
timeout: options.timeout,
queue: options.queue.as_ref().map(OwnedEnqueue::from),
input: Some(encode(&input, "argument")?),
attributes: encode_attributes(options.attributes)?,
started_at: Timestamp::now(),
};
let (executor, placement) = placed.into_parts();
let creating = create::<R, E>(
Arc::clone(&executor),
placement,
self.key().clone(),
request,
)
.instrument(tracing::Span::current());
match spawn_tracked(&executor, creating).await {
Ok(started) => started,
Err(join) if join.is_cancelled() => Err(Error::Interrupted { workflow_id }),
Err(join) => std::panic::resume_unwind(join.into_panic()),
}
}
}
fn child_workflow_id(chosen: Option<&str>, parent: Option<&Parent<'_>>) -> String {
match (chosen, parent) {
(Some(id), _) => id.to_owned(),
(None, Some(parent)) => format!("{}-{}", parent.workflow_id(), parent.step_id),
(None, None) => uuid::Uuid::new_v4().to_string(),
}
}
async fn create<R, E>(
executor: Arc<Executor>,
placement: StepPlacement,
key: WorkflowKey,
request: OwnedStart,
) -> Result<WorkflowHandle<R, E>> {
let placed = StartPlacement::of(executor, placement);
let executor = &placed.executor;
let OwnedStart {
workflow_id,
timeout,
queue,
input,
attributes,
started_at,
} = request;
let owned_queue = queue.as_ref().map(OwnedEnqueue::as_enqueue);
let enqueue = owned_queue.as_ref();
let parent = placed.parent();
if let Some(parent) = &parent
&& let Some(child) = parent.recorded_child(executor, &key.name).await?
{
tracing::debug!(
parent_workflow_id = parent.workflow_id(),
step_id = parent.step_id,
workflow_id = child,
"the child workflow was already started; the handle joins it"
);
return Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
child,
false,
));
}
let deadline = match (timeout, &parent) {
(Timeout::Explicit(_), _) if queue.is_some() => None,
(Timeout::Explicit(timeout), _) => Timestamp::now().checked_add(timeout),
(Timeout::None, _) => None,
(Timeout::Inherit, Some(parent)) => parent.deadline(),
(Timeout::Inherit, None) => None,
};
let new = NewWorkflow {
name: Some(&key.name),
class_name: key.class_name.as_deref(),
config_name: key.config_name.as_deref(),
input: input.as_deref(),
serialization: Some(executor.serializer().name()),
executor_id: Some(executor.executor_id()),
application_name: Some(executor.app_name()),
application_version: Some(executor.app_version()),
timeout: timeout.budget(),
deadline,
attributes: attributes.as_deref(),
..new_row(&workflow_id, enqueue)
};
let initialized = match init_or_join(
executor.connection(),
&new,
enqueue.map_or(DuplicationPolicy::Reject, |enqueue| {
enqueue.duplication_policy
}),
parent.as_ref().map(|parent| InitWorkflowCaller {
parent_workflow_id: parent.workflow_id(),
step_id: parent.step_id,
step_name: &key.name,
started_at,
}),
)
.await?
{
Submitted::Created(initialized) => initialized,
Submitted::Joined(holder) => {
if let Some(parent) = &parent {
parent
.record_child(executor, &holder, &key.name, started_at)
.await?;
}
return Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
holder,
true,
));
}
};
if let Some(enqueue) = enqueue {
tracing::debug!(
workflow_id,
queue = enqueue.name,
"the workflow is enqueued"
);
return Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
workflow_id,
true,
));
}
if !initialized.should_execute {
tracing::debug!(
workflow_id,
"the workflow is already owned; the handle joins the existing run"
);
return Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
workflow_id,
true,
));
}
let task = spawn_execution(
executor,
key.clone(),
workflow_id.clone(),
input,
initialized.deadline,
None,
);
Ok(WorkflowHandle::local(
Arc::clone(executor.connection()),
workflow_id,
task,
))
}
#[must_use = "a start or a run that is not awaited never starts the workflow, and has still spent \
the step ids it claimed; await it"]
pub struct PendingWorkflow<'a, T, E = crate::EngineOnly>(PendingStep<'a, T, E>);
#[must_use = "a start that is not awaited never starts the workflow, and has still spent the step \
id it claimed; await it"]
pub struct PendingStart<'a, R, E = crate::EngineOnly, C = E> {
starting: PendingWorkflow<'a, WorkflowHandle<R, E>, crate::EngineOnly>,
channel: PhantomData<fn() -> C>,
}
impl<'a, R, E, C> PendingStart<'a, R, E, C> {
#[must_use]
pub fn step_id(&self) -> Option<i32> {
self.starting.step_id()
}
pub fn lift<C2>(self) -> PendingStart<'a, R, E, C2> {
PendingStart {
starting: self.starting,
channel: PhantomData,
}
}
}
impl<R, E, C> Future for PendingStart<'_, R, E, C> {
type Output = Result<WorkflowHandle<R, E>, C>;
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.get_mut().starting)
.poll(cx)
.map(|started| started.map_err(Error::lift))
}
}
impl<R, E, C> std::fmt::Debug for PendingStart<'_, R, E, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PendingStart").field(&self.starting).finish()
}
}
pub type PendingRun<'a, R, E = crate::EngineOnly> = PendingWorkflow<'a, R, E>;
impl<T, E> PendingWorkflow<'_, T, E> {
#[must_use]
pub fn step_id(&self) -> Option<i32> {
self.0.step_id()
}
}
impl<T, E> Future for PendingWorkflow<'_, T, E> {
type Output = Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.get_mut().0).poll(cx)
}
}
impl<T, E> std::fmt::Debug for PendingWorkflow<'_, T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PendingWorkflow").field(&self.0).finish()
}
}
struct StartPlacement {
executor: Arc<Executor>,
placement: StepPlacement,
}
impl StartPlacement {
fn of(executor: Arc<Executor>, placement: StepPlacement) -> Self {
Self {
executor,
placement,
}
}
fn into_parts(self) -> (Arc<Executor>, StepPlacement) {
(self.executor, self.placement)
}
fn parent(&self) -> Option<Parent<'_>> {
match &self.placement {
StepPlacement::Recorded { ctx, step_id } => Some(Parent {
ctx,
step_id: *step_id,
}),
StepPlacement::Outside
| StepPlacement::InsideStep { .. }
| StepPlacement::ClientConnection => None,
}
}
}
struct Parent<'a> {
ctx: &'a Ctx,
step_id: i32,
}
impl Parent<'_> {
fn workflow_id(&self) -> &str {
self.ctx.workflow_id()
}
fn deadline(&self) -> Option<Timestamp> {
self.ctx.deadline()
}
async fn recorded_child(&self, executor: &Executor, step_name: &str) -> Result<Option<String>> {
let Some(recorded) = executor
.sysdb()
.check_step(self.workflow_id(), self.step_id, step_name)
.await
.map_err(Error::SystemDatabase)?
else {
return Ok(None);
};
recorded.child_workflow_id.map(Some).ok_or_else(|| {
Error::SystemDatabase(crate::sysdb::Error::UnexpectedStep {
workflow_id: self.workflow_id().to_owned(),
step_id: self.step_id,
expected: format!("a child workflow start of {step_name}"),
recorded: format!("a plain step named {step_name}"),
})
})
}
async fn record_child(
&self,
executor: &Executor,
child_workflow_id: &str,
step_name: &str,
started_at: Timestamp,
) -> Result<()> {
executor
.sysdb()
.record_child_workflow(
self.workflow_id(),
child_workflow_id,
self.step_id,
step_name,
Some(started_at),
)
.await
.map_err(Error::SystemDatabase)
}
}
pub(crate) fn spawn_execution(
executor: &Arc<Executor>,
key: WorkflowKey,
workflow_id: String,
input: Option<String>,
deadline: Option<Timestamp>,
slot: Option<crate::dequeue::Slot>,
) -> tokio::task::JoinHandle<std::result::Result<Option<String>, Failure>> {
let span = tracing::info_span!("workflow", workflow_id = %workflow_id, name = %key);
spawn_tracked(
executor,
{
let executor = Arc::clone(executor);
async move {
let _slot = slot;
let ctx = Ctx::new(Arc::clone(&executor), &workflow_id, deadline);
let _panic_log = PanicLog {
workflow_id: &workflow_id,
};
execute(&executor, &key, &workflow_id, input, ctx).await
}
}
.instrument(span),
)
}
async fn run_until_deadline<F>(
executor: &Executor,
workflow_id: &str,
deadline: Option<Timestamp>,
body: F,
) -> Ended
where
F: Future<Output = std::result::Result<Option<String>, Failure>>,
{
let Some(deadline) = deadline else {
return Ended::Body(body.await);
};
let remaining = deadline
.duration_since(Timestamp::now())
.unwrap_or(Duration::ZERO);
tokio::select! {
biased;
outcome = body => Ended::Body(outcome),
() = tokio::time::sleep(remaining) => {
Ended::Terminal(cancel_at_deadline(executor, workflow_id).await)
}
}
}
enum Ended {
Body(std::result::Result<Option<String>, Failure>),
Terminal(std::result::Result<Option<String>, Failure>),
}
async fn cancel_at_deadline(
executor: &Executor,
workflow_id: &str,
) -> std::result::Result<Option<String>, Failure> {
let cancelled = || {
Err(Failure::Control(Error::WorkflowCancelled {
workflow_id: workflow_id.to_owned(),
}))
};
tracing::info!(
workflow_id,
"the workflow exceeded its deadline and is cancelled"
);
match executor
.sysdb()
.cancel_workflows(&[workflow_id], false, None)
.await
{
Ok(moved) if moved.iter().any(|id| id == workflow_id) => cancelled(),
Ok(_) => {
tracing::warn!(
workflow_id,
"the deadline fired on a workflow another execution had already finished; its \
recorded outcome stands"
);
executor.connection().adopt(workflow_id, true).await
}
Err(error) => {
tracing::error!(
workflow_id,
%error,
"could not record the deadline cancellation; the row stays PENDING for recovery"
);
cancelled()
}
}
}
async fn execute(
executor: &Executor,
key: &WorkflowKey,
workflow_id: &str,
input: Option<String>,
ctx: Ctx,
) -> std::result::Result<Option<String>, Failure> {
let workflow = executor
.workflows()
.get(key)
.ok_or_else(|| {
Failure::Control(Error::NotRegistered {
key: key.to_string(),
})
})?
.clone();
let deadline = ctx.deadline();
let outcome = match run_until_deadline(
executor,
workflow_id,
deadline,
Ctx::scope(ctx, workflow(input)),
)
.await
{
Ended::Body(outcome) => outcome,
Ended::Terminal(settled) => return settled,
};
let write = match &outcome {
Ok(output) => {
executor
.sysdb()
.record_workflow_outcome(workflow_id, Outcome::Output(output.as_deref()))
.await
}
Err(Failure::Control(control)) => {
tracing::warn!(
error = %control,
"a control signal ended this execution: it records no outcome of its own"
);
return outcome;
}
Err(Failure::Recorded(encoded)) => {
executor
.sysdb()
.record_workflow_outcome(workflow_id, Outcome::Error(encoded))
.await
}
}
.map_err(|error| {
tracing::warn!(
error = %error,
"could not record the workflow's outcome: the row stays PENDING"
);
Failure::Control(Error::SystemDatabase(error))
})?;
match write {
OutcomeWrite::Recorded => {
match &outcome {
Ok(_) => tracing::debug!("the workflow completed; its output is recorded"),
Err(_) => tracing::debug!("the workflow failed; its error is recorded"),
}
outcome
}
OutcomeWrite::AlreadyFinished => {
tracing::warn!(
workflow_id,
"another execution recorded this workflow's outcome first"
);
executor.connection().adopt(workflow_id, true).await
}
}
}
impl Connection {
pub(crate) async fn adopt(
&self,
workflow_id: &str,
fail_if_missing: bool,
) -> std::result::Result<Option<String>, Failure> {
let outcome = self
.sysdb()
.await_workflow_result(workflow_id, self.outcome_poll_interval(), fail_if_missing)
.await
.map_err(|error| {
Failure::Control(match error {
crate::sysdb::Error::NonExistentWorkflow { .. } => Error::WorkflowNotFound {
workflow_id: workflow_id.to_owned(),
},
other => Error::SystemDatabase(other),
})
})?;
match outcome {
AwaitedOutcome::Succeeded { output, .. } => Ok(output),
AwaitedOutcome::Failed { error, .. } => Err(Failure::Recorded(error)),
AwaitedOutcome::Cancelled => Err(Failure::Control(Error::WorkflowCancelled {
workflow_id: workflow_id.to_owned(),
})),
AwaitedOutcome::Parked { recovery_attempts } => {
Err(Failure::Control(Error::MaxRecoveryAttemptsExceeded {
workflow_id: workflow_id.to_owned(),
recovery_attempts,
}))
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use super::*;
struct Capture(Arc<Mutex<Vec<String>>>);
impl tracing::Subscriber for Capture {
fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
struct Message(String);
impl tracing::field::Visit for Message {
fn record_debug(
&mut self,
field: &tracing::field::Field,
value: &dyn std::fmt::Debug,
) {
if field.name() == "message" {
use std::fmt::Write;
let _ = write!(self.0, "{value:?}");
}
}
}
let mut message = Message(format!("{} ", event.metadata().level()));
event.record(&mut message);
self.0.lock().unwrap().push(message.0);
}
fn enter(&self, _: &tracing::span::Id) {}
fn exit(&self, _: &tracing::span::Id) {}
}
#[tokio::test]
async fn a_panic_in_a_spawned_task_is_logged_from_the_unwind() {
let events = Arc::new(Mutex::new(Vec::new()));
let _guard = tracing::subscriber::set_default(Capture(Arc::clone(&events)));
let task = tokio::spawn(async {
let _log = PanicLog {
workflow_id: "wf-boom",
};
tokio::task::yield_now().await;
panic!("a bug, not an outcome");
});
let joined = task.await.expect_err("the task must panic");
assert!(joined.is_panic());
let events = events.lock().unwrap();
assert!(
events
.iter()
.any(|e| e.starts_with("ERROR") && e.contains("panicked")),
"the unwind must produce the error event: {events:?}"
);
}
#[tokio::test]
async fn abort_all_waits_until_every_task_has_departed() {
let tasks = Arc::new(Tasks::default());
tasks.arrived();
let mut sweep = {
let tasks = Arc::clone(&tasks);
tokio::spawn(async move { tasks.abort_all().await })
};
assert!(
tokio::time::timeout(Duration::from_millis(100), &mut sweep)
.await
.is_err(),
"the sweep returned while a task was still live"
);
tasks.departed();
tokio::time::timeout(Duration::from_secs(10), sweep)
.await
.expect("the sweep never noticed the last task leaving")
.expect("the sweep panicked");
}
#[tokio::test]
async fn a_task_arriving_after_the_sweep_is_cancelled() {
let tasks = Tasks::default();
tasks.abort_all().await;
let task = tokio::spawn(std::future::pending::<()>());
tasks.insert(task.abort_handle());
let joined = tokio::time::timeout(Duration::from_secs(10), task)
.await
.expect("the late task was never cancelled");
assert!(joined.expect_err("it must be cancelled").is_cancelled());
}
#[tokio::test]
async fn a_completed_task_logs_no_panic() {
let events = Arc::new(Mutex::new(Vec::new()));
let _guard = tracing::subscriber::set_default(Capture(Arc::clone(&events)));
tokio::spawn(async {
let _panic_log = PanicLog {
workflow_id: "wf-fine",
};
tokio::task::yield_now().await;
})
.await
.expect("the task must complete");
let events = events.lock().unwrap();
assert!(events.is_empty(), "{events:?}");
}
#[tokio::test]
async fn a_start_built_in_another_workflow_is_refused() {
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("smuggled-start", db.url())
});
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, crate::Error>(n) })
.expect("registration failed");
dbos.launch().await.expect("launch failed");
let executor = dbos.executor("test").expect("launched");
#[allow(clippy::async_yields_async)]
let smuggled = Ctx::scope(Ctx::new(Arc::clone(&executor), "wf-donor", None), async {
child.start(1)
})
.await;
assert_eq!(smuggled.step_id(), Some(0), "the donor's counter moved");
let thief = Ctx::new(Arc::clone(&executor), "wf-thief", None);
match Ctx::scope(thief, smuggled).await {
Err(Error::StepBuiltElsewhere {
step,
built,
polled,
}) => {
assert_eq!((step.as_str(), &*built), ("child", "in workflow wf-donor"));
assert_eq!(&*polled, "in workflow wf-thief");
}
other => panic!("expected a built-elsewhere refusal, got {other:?}"),
}
assert!(
executor
.sysdb()
.get_workflow("wf-donor-0")
.await
.expect("read failed")
.is_none(),
"the refused start created no child"
);
dbos.shutdown().await;
}
}