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, Instant};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tracing::Instrument;
use crate::checkpoint::{PendingStep, StepPlacement};
use crate::connection::Connection;
use crate::context::Ctx;
use crate::error::{DurableError, EncodedError, Error, Failure, Result, refusal_detail};
use crate::handle::{ChildResultPlacement, WorkflowHandle};
use crate::instance::Executor;
use crate::registry::{QualifiedWorkflowName, WorkflowRef};
use crate::rt::AbortHandle;
use crate::serialization::{EncodedOutput, PayloadEncoding, SerializationFormat, encode_input};
use crate::sysdb::types::{
AwaitedOutcome, DatabaseTime, InitWorkflowCaller, NewWorkflow, Outcome, OutcomeWrite,
StepCaller, StepTiming, Timestamp, WorkflowIdReusePolicy, WorkflowInitResult,
decode_step_value, encode_step_value, validate_positive_duration,
};
pub(crate) const MAX_RECOVERY_ATTEMPTS: i64 = 100;
#[derive(Default)]
pub(crate) struct Tasks {
state: Mutex<State>,
emptied: tokio::sync::Notify,
}
#[derive(Default)]
struct State {
running: Vec<(AbortHandle, String)>,
live: usize,
closed: bool,
}
impl Tasks {
fn insert(&self, handle: AbortHandle, label: String) {
let mut state = self.lock();
if state.closed {
drop(state);
handle.abort();
return;
}
state.running.retain(|(handle, _)| !handle.is_finished());
state.running.push((handle, label));
}
fn arrived(&self) {
self.lock().live += 1;
}
fn departed(&self) {
let emptied = {
let mut state = self.lock();
state.live = state.live.saturating_sub(1);
state.live == 0
};
if emptied {
self.emptied.notify_waiters();
}
}
pub(crate) fn running(&self) -> Vec<String> {
self.lock()
.running
.iter()
.filter(|(handle, _)| !handle.is_finished())
.map(|(_, label)| label.clone())
.collect()
}
pub(crate) async fn wait_all_until(&self, deadline: Instant) -> bool {
crate::rt::timeout_at(deadline, self.wait_all())
.await
.is_ok()
}
pub(crate) async fn abort_all(&self) -> Vec<String> {
let cancelled = {
let mut state = self.lock();
state.closed = true;
let mut cancelled = Vec::new();
for (handle, label) in state.running.drain(..) {
if !handle.is_finished() {
handle.abort();
cancelled.push(label);
}
}
cancelled
};
self.wait_all().await;
cancelled
}
async fn wait_all(&self) {
loop {
let emptied = self.emptied.notified();
let live = self.lock().live;
if live == 0 {
return;
}
emptied.await;
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Role {
Producer,
Workflow,
Monitor,
}
struct TaskGuard(Arc<Executor>, Role);
impl Drop for TaskGuard {
fn drop(&mut self) {
self.0.tasks(self.1).departed();
}
}
pub(crate) fn spawn_tracked<F>(
executor: &Arc<Executor>,
role: Role,
label: String,
future: F,
) -> crate::rt::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let tasks = executor.tasks(role);
tasks.arrived();
let guard = TaskGuard(Arc::clone(executor), role);
let task = executor.runtime().spawn(async move {
let _guard = guard;
future.await
});
tasks.insert(task.abort_handle(), label);
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 workflow_id_reuse_policy: WorkflowIdReusePolicy,
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 workflow_id_reuse_policy: WorkflowIdReusePolicy,
pub timeout: Timeout,
pub queue: Option<QueueRequest<'a>>,
pub attributes: Option<&'a serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueueRequest<'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,
pub serialization: SerializationFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DuplicationPolicy {
#[default]
Reject,
ReturnExisting,
}
impl<'a> QueueRequest<'a> {
pub fn new(name: &'a str) -> Self {
Self {
name,
deduplication_id: None,
priority: None,
partition_key: None,
delay: None,
duplication_policy: DuplicationPolicy::default(),
serialization: SerializationFormat::Default,
}
}
pub(crate) fn validate(&self) -> Result<()> {
let refused = |message: String| start_refusal(Some(self.name), message);
let refuse = |message: String| Err(refused(message));
for (field, value) in [
("deduplication_id", self.deduplication_id),
("partition_key", self.partition_key),
] {
if value == Some("") {
return refuse(format!("`{field}` must be absent rather than empty"));
}
}
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
));
}
}
if let Some(delay) = self.delay {
validate_positive_duration("delay", delay).map_err(|error| {
Error::from_refusal(error, |field, detail| {
refused(refusal_detail(field, &detail))
})
})?;
}
Ok(())
}
pub(crate) fn stored_priority(&self) -> i32 {
self.priority.map_or(0, |priority| priority as i32)
}
}
pub(crate) const START_OPERATION: &str = "start a workflow";
pub(crate) const ENQUEUE_OPERATION: &str = "enqueue a workflow";
fn start_refusal(queue: Option<&str>, detail: String) -> Error {
match queue {
Some(queue) => Error::InvalidArgument {
operation: ENQUEUE_OPERATION.into(),
detail: format!("queue `{queue}`: {detail}"),
},
None => Error::InvalidArgument {
operation: START_OPERATION.into(),
detail,
},
}
}
pub(crate) fn refused_start(queue: Option<&str>, error: Error) -> Error {
match error {
Error::SystemDatabase(crate::sysdb::Error::InvalidInput { field, detail }) => {
start_refusal(queue, refusal_detail(api_field(&field), &detail))
}
other => other,
}
}
fn api_field(field: &str) -> &str {
match field {
"queue_name" => "name",
"queue_partition_key" => "partition_key",
"name" => "workflow",
"application_version" => "app_version",
other => other,
}
}
pub(crate) fn validate_start(
queue: Option<&QueueRequest<'_>>,
timeout: Option<Duration>,
workflow_id: Option<&str>,
reuse_policy: WorkflowIdReusePolicy,
) -> Result<()> {
if workflow_id == Some("") {
return Err(start_refusal(
queue.map(|queue| queue.name),
"`workflow_id` must not be empty".to_owned(),
));
}
if reuse_policy == WorkflowIdReusePolicy::Reject && workflow_id.is_none() {
return Err(start_refusal(
queue.map(|queue| queue.name),
"`WorkflowIdReusePolicy::Reject` needs a `workflow_id`: the policy is about an id the \
caller chose"
.to_owned(),
));
}
if let Some(queue) = queue {
queue.validate()?;
}
if let Some(timeout) = timeout {
validate_positive_duration("timeout", timeout).map_err(|error| {
Error::from_refusal(error, |field, detail| {
start_refusal(
queue.map(|queue| queue.name),
refusal_detail(field, &detail),
)
})
})?;
}
Ok(())
}
pub(crate) fn new_workflow<'a>(
workflow_id: &'a str,
queue: Option<&QueueRequest<'a>>,
) -> NewWorkflow<'a> {
NewWorkflow {
queue_name: queue.map(|queue| queue.name),
deduplication_id: queue.and_then(|queue| queue.deduplication_id),
priority: queue.map_or(0, QueueRequest::stored_priority),
queue_partition_key: queue.and_then(|queue| queue.partition_key),
delay: queue.and_then(|queue| queue.delay),
..NewWorkflow::new(workflow_id)
}
}
pub(crate) struct OwnedStart {
pub(crate) workflow_id: String,
pub(crate) workflow_id_reuse_policy: WorkflowIdReusePolicy,
pub(crate) timeout: Timeout,
pub(crate) queue: Option<OwnedQueueRequest>,
pub(crate) input: Option<String>,
pub(crate) encoding: PayloadEncoding,
pub(crate) attributes: Option<String>,
pub(crate) started_at: Timestamp,
pub(crate) debounce: Option<Debounced>,
pub(crate) app_name: Option<String>,
pub(crate) app_version: TargetVersion,
}
pub(crate) enum TargetVersion {
Executor,
Named(String),
Latest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Debounced {
pub(crate) timeout: Option<Duration>,
}
pub(crate) struct OwnedQueueRequest {
name: String,
deduplication_id: Option<String>,
priority: Option<u32>,
partition_key: Option<String>,
delay: Option<Duration>,
duplication_policy: DuplicationPolicy,
serialization: SerializationFormat,
}
impl From<&QueueRequest<'_>> for OwnedQueueRequest {
fn from(enqueue: &QueueRequest<'_>) -> 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,
serialization: enqueue.serialization,
}
}
}
impl OwnedQueueRequest {
fn as_queue_request(&self) -> QueueRequest<'_> {
QueueRequest {
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,
serialization: self.serialization,
}
}
}
pub(crate) enum Submitted {
Created(WorkflowInitResult),
Joined(String),
}
pub(crate) async fn init_or_join(
conn: &Connection,
new: &NewWorkflow<'_>,
policy: DuplicationPolicy,
reuse_policy: WorkflowIdReusePolicy,
caller: Option<InitWorkflowCaller<'_>>,
) -> Result<Submitted> {
loop {
match conn.sysdb().init_workflow(new, caller, reuse_policy).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::from_execution(error)),
}
}
}
pub(crate) fn encode_attributes(
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Result<Option<String>> {
attributes.map(encode_attribute_map).transpose()
}
pub(crate) fn encode_attribute_map(
attributes: &serde_json::Map<String, serde_json::Value>,
) -> Result<String> {
serde_json::to_string(attributes).map_err(|source| Error::encode_failed("attributes", source))
}
impl<'a> From<RunOptions<'a>> for StartOptions<'a> {
fn from(options: RunOptions<'a>) -> Self {
Self {
workflow_id: options.workflow_id,
workflow_id_reuse_policy: options.workflow_id_reuse_policy,
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 {
pub(crate) 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.name());
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.executor(START_OPERATION)?;
validate_start(
options.queue.as_ref(),
options.timeout.budget(),
options.workflow_id,
options.workflow_id_reuse_policy,
)?;
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.name());
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 tracked = placed.parent().map(|parent| parent.ctx.starts().token());
let (executor, placement) = placed.into_parts();
let registered =
PayloadEncoding::resolve(self.serialization(), executor.serializer().encoding());
let encoding = match &options.queue {
Some(enqueue) => PayloadEncoding::resolve(enqueue.serialization, registered),
None => registered,
};
let request = OwnedStart {
workflow_id: workflow_id.clone(),
workflow_id_reuse_policy: options.workflow_id_reuse_policy,
timeout: options.timeout,
queue: options.queue.as_ref().map(OwnedQueueRequest::from),
input: Some(encode_input(&input, &encoding)?),
encoding,
attributes: encode_attributes(options.attributes)?,
started_at: Timestamp::now(),
debounce: None,
app_name: None,
app_version: TargetVersion::Executor,
};
let creating = create::<R, E>(
Arc::clone(&executor),
placement,
self.qualified_name().clone(),
request,
)
.instrument(tracing::Span::current());
let creating = async move {
let _tracked = tracked;
creating.await
};
let queue = options.queue.as_ref().map(|enqueue| enqueue.name);
let label = format!("start {workflow_id}");
match spawn_tracked(&executor, Role::Workflow, label, creating).await {
Ok(started) => started.map_err(|error| refused_start(queue, error)),
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(),
}
}
pub(crate) fn placed_workflow_id(chosen: Option<&str>, placement: &StepPlacement) -> String {
match placement {
StepPlacement::Recorded { ctx, step_id } => child_workflow_id(
chosen,
Some(&Parent {
ctx,
step_id: *step_id,
}),
),
StepPlacement::Outside
| StepPlacement::InsideStep { .. }
| StepPlacement::ClientConnection => child_workflow_id(chosen, None),
}
}
pub(crate) async fn create<R, E>(
executor: Arc<Executor>,
placement: StepPlacement,
key: QualifiedWorkflowName,
request: OwnedStart,
) -> Result<WorkflowHandle<R, E>> {
let placed = StartPlacement::of(executor, placement);
let executor = &placed.executor;
let OwnedStart {
workflow_id,
workflow_id_reuse_policy,
timeout,
queue,
input,
encoding,
attributes,
started_at,
debounce,
app_name,
app_version,
} = request;
let owned_queue = queue.as_ref().map(OwnedQueueRequest::as_queue_request);
let enqueue = owned_queue.as_ref();
let parent = placed.parent();
if let Some(parent) = &parent
&& let Some(child) = parent.check_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(_) | Timeout::None, _) => None,
(Timeout::Inherit, Some(parent)) => parent.deadline(),
(Timeout::Inherit, None) => None,
};
let for_another_app = app_name
.as_deref()
.is_some_and(|name| name != executor.app_name());
let new = NewWorkflow {
name: Some(key.name()),
class_name: key.class_name(),
config_name: key.config_name(),
input: input.as_deref(),
serialization: Some(encoding.name()),
executor_id: Some(executor.executor_id()),
application_name: Some(app_name.as_deref().unwrap_or(executor.app_name())),
application_version: match &app_version {
TargetVersion::Executor => (!for_another_app).then(|| executor.app_version()),
TargetVersion::Named(version) => Some(version.as_str()),
TargetVersion::Latest => None,
},
timeout: timeout.budget(),
deadline,
attributes: attributes.as_deref(),
is_debounced: debounce.is_some(),
debounce_timeout: debounce.and_then(|debounce| debounce.timeout),
..new_workflow(&workflow_id, enqueue)
};
let initialized = match init_or_join(
executor.connection(),
&new,
enqueue.map_or(DuplicationPolicy::Reject, |enqueue| {
enqueue.duplication_policy
}),
workflow_id_reuse_policy,
parent.as_ref().map(|parent| InitWorkflowCaller {
parent_workflow_id: parent.workflow_id(),
step_id: parent.step_id,
step_name: key.name(),
started_at,
owner_xid: parent.owner_xid(),
}),
)
.await
{
Ok(Submitted::Created(initialized)) => initialized,
Err(Error::SystemDatabase(error)) if should_record_start_error(&error) => {
return Err(match &parent {
Some(parent) => {
parent
.record_start_error(executor, error, key.name(), started_at)
.await
}
None => Error::SystemDatabase(error),
});
}
Err(error) => return Err(error),
Ok(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,
"another caller created the workflow; the handle joins it"
);
return Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
workflow_id,
true,
));
}
let serialization = initialized.serialization;
let task = spawn_execution(
executor,
key.clone(),
workflow_id.clone(),
initialized.creator_xid,
input,
serialization,
Deadline::of(initialized.deadline, initialized.clock),
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 call that is not awaited never sets the workflow running, 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> {
pub(crate) fn from_step(
step: PendingStep<'a, WorkflowHandle<R, E>, crate::EngineOnly>,
) -> Self {
Self {
starting: PendingWorkflow(step),
channel: PhantomData,
}
}
#[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,
}
}
}
fn should_record_start_error(error: &crate::sysdb::Error) -> bool {
matches!(
error,
crate::sysdb::Error::QueueDeduplicated { .. }
| crate::sysdb::Error::ConflictingWorkflow { .. }
| crate::sysdb::Error::WorkflowIdInUse { .. }
| crate::sysdb::Error::InvalidInput { .. }
)
}
const RECORDED_START_ERRORS: [&str; 4] = [
"QueueDeduplicated",
"ConflictingWorkflow",
"WorkflowIdInUse",
"InvalidInput",
];
fn is_recorded_start_error(value: &serde_json::Value) -> bool {
value.as_object().is_some_and(|object| {
object.len() == 1
&& object
.keys()
.all(|key| RECORDED_START_ERRORS.contains(&key.as_str()))
})
}
struct Parent<'a> {
ctx: &'a Ctx,
step_id: i32,
}
impl Parent<'_> {
fn workflow_id(&self) -> &str {
self.ctx.workflow_id()
}
fn owner_xid(&self) -> &str {
self.ctx.owner_xid()
}
fn deadline(&self) -> Option<Timestamp> {
self.ctx.deadline()
}
async fn check_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::from_execution)?
else {
return Ok(None);
};
if let Some(child) = recorded.child_workflow_id {
return Ok(Some(child));
}
if let Some(encoded) = recorded.error.as_deref() {
let caller = StepCaller {
workflow_id: self.workflow_id(),
step_id: self.step_id,
codec: executor.serializer(),
owner_xid: self.owner_xid(),
};
let value: serde_json::Value =
decode_step_value(&recorded, encoded, caller, step_name, "an error")
.map_err(Error::SystemDatabase)?;
if is_recorded_start_error(&value) {
let error = serde_json::from_value::<crate::sysdb::Error>(value).map_err(|e| {
Error::SystemDatabase(crate::sysdb::Error::Malformed(format!(
"workflow {} step {} ({step_name}) has an error this build cannot read: \
{e}",
self.workflow_id(),
self.step_id,
)))
})?;
tracing::debug!(
parent_workflow_id = self.workflow_id(),
step_id = self.step_id,
%error,
"the child workflow start failed; the replay returns the same error"
);
return Err(Error::SystemDatabase(error));
}
}
Err(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_start_error(
&self,
executor: &Executor,
error: crate::sysdb::Error,
step_name: &str,
started_at: Timestamp,
) -> Error {
let recorded = match encode_step_value(&error, executor.serializer(), "an error") {
Ok(recorded) => recorded,
Err(encode_error) => {
tracing::warn!(
parent_workflow_id = self.workflow_id(),
step_id = self.step_id,
%error,
%encode_error,
"the child workflow start failed, but its error could not be encoded \
to record it"
);
return Error::SystemDatabase(encode_error);
}
};
let timing = StepTiming {
started_at,
completed_at: Timestamp::now(),
};
match executor
.sysdb()
.record_step(
self.workflow_id(),
self.step_id,
step_name,
Outcome::Error(&recorded.value),
recorded.serialization.as_deref(),
Some(timing),
self.owner_xid(),
)
.await
{
Ok(()) => Error::SystemDatabase(error),
Err(record_error) => Error::from_execution(record_error),
}
}
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),
self.owner_xid(),
)
.await
.map_err(Error::from_execution)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_execution(
executor: &Arc<Executor>,
key: QualifiedWorkflowName,
workflow_id: String,
owner_xid: String,
input: Option<String>,
serialization: Option<String>,
deadline: Option<Deadline>,
slot: Option<crate::dequeue::Slot>,
) -> crate::rt::JoinHandle<std::result::Result<EncodedOutput, Failure>> {
let span = tracing::info_span!("workflow", workflow_id = %workflow_id, name = %key);
spawn_tracked(
executor,
Role::Workflow,
workflow_id.clone(),
{
let executor = Arc::clone(executor);
async move {
let encoding = PayloadEncoding::of(serialization.as_deref(), executor.serializer())
.map_err(|error| {
tracing::warn!(
%error,
"the workflow's input is in an encoding this SDK cannot read; it \
records no outcome"
);
Failure::Control(error)
})?;
let ctx = Ctx::new(
Arc::clone(&executor),
&workflow_id,
owner_xid,
deadline.map(|deadline| deadline.at),
encoding.clone(),
);
let _panic_log = PanicLog {
workflow_id: &workflow_id,
};
execute(
&executor,
&key,
&workflow_id,
input,
encoding,
ctx,
deadline.map(|deadline| deadline.expires),
slot,
)
.await
}
}
.instrument(span),
)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Deadline {
at: Timestamp,
expires: Instant,
}
impl Deadline {
pub(crate) fn of(at: Option<Timestamp>, clock: DatabaseTime) -> Option<Self> {
at.map(|at| Self {
at,
expires: clock.reaches(at),
})
}
}
async fn run_until_deadline<F>(expires: Option<Instant>, body: F) -> Ended
where
F: Future<Output = std::result::Result<EncodedOutput, Failure>>,
{
let Some(expires) = expires else {
return Ended::Body(body.await);
};
tokio::select! {
biased;
outcome = body => Ended::Body(outcome),
() = crate::rt::sleep_until(expires) => Ended::DeadlinePassed,
}
}
enum Ended {
Body(std::result::Result<EncodedOutput, Failure>),
DeadlinePassed,
}
async fn time_out(
executor: &Executor,
workflow_id: &str,
owner_xid: &str,
slot: Option<crate::dequeue::Slot>,
) -> std::result::Result<EncodedOutput, Failure> {
drop(slot);
match executor
.sysdb()
.cancel_if_timed_out(workflow_id, owner_xid)
.await
{
Ok(true) => {
tracing::info!(
workflow_id,
"the workflow exceeded its deadline and is cancelled"
);
}
Ok(false) => {}
Err(error) => {
tracing::warn!(workflow_id, %error, "could not record the deadline cancellation");
}
}
executor.connection().adopt(workflow_id, true).await
}
#[allow(clippy::too_many_arguments)]
async fn execute(
executor: &Executor,
key: &QualifiedWorkflowName,
workflow_id: &str,
input: Option<String>,
encoding: PayloadEncoding,
ctx: Ctx,
expires: Option<Instant>,
slot: Option<crate::dequeue::Slot>,
) -> std::result::Result<EncodedOutput, Failure> {
let workflow = executor
.workflows()
.get(key)
.ok_or_else(|| {
Failure::Control(Error::NotRegistered {
workflow: key.clone(),
})
})?
.body
.clone();
let owner_xid = ctx.owner_xid().to_owned();
let starts = ctx.starts().clone();
let outcome = match run_until_deadline(
expires,
Ctx::scope(ctx, async move {
let outcome = async {
let encoded = workflow(input, encoding.clone()).await?;
Ok(EncodedOutput { encoded, encoding })
}
.await;
starts.drained().await;
outcome
}),
)
.await
{
Ended::Body(outcome) => outcome,
Ended::DeadlinePassed => return time_out(executor, workflow_id, &owner_xid, slot).await,
};
let write = match &outcome {
Ok(output) => {
executor
.sysdb()
.record_workflow_outcome(
workflow_id,
Outcome::Output(output.encoded.as_deref()),
&owner_xid,
)
.await
}
Err(Failure::Control(
Error::SystemDatabase(crate::sysdb::Error::WorkflowConflict {
workflow_id: stopped,
})
| Error::WorkflowCancelled {
workflow_id: stopped,
},
)) if stopped == workflow_id => {
tracing::warn!(
workflow_id,
"this execution was cancelled or superseded; reading the workflow's outcome"
);
return adopt_superseded(executor, workflow_id, slot).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(recorded)) => {
executor
.sysdb()
.record_workflow_outcome(workflow_id, Outcome::Error(&recorded.encoded), &owner_xid)
.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"
);
adopt_superseded(executor, workflow_id, slot).await
}
}
}
async fn adopt_superseded(
executor: &Executor,
workflow_id: &str,
slot: Option<crate::dequeue::Slot>,
) -> std::result::Result<EncodedOutput, Failure> {
drop(slot);
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<EncodedOutput, 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),
})
})?;
let encoding = |serialization: Option<String>| {
PayloadEncoding::of(serialization.as_deref(), self.serializer())
.map_err(Failure::Control)
};
match outcome {
AwaitedOutcome::Succeeded {
output,
serialization,
} => Ok(EncodedOutput {
encoded: output,
encoding: encoding(serialization)?,
}),
AwaitedOutcome::Failed {
error,
serialization,
} => Err(Failure::Recorded(EncodedError {
encoded: error,
encoding: encoding(serialization)?,
})),
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::*;
#[test]
fn a_start_refuses_a_delay_or_timeout_the_row_cannot_hold() {
let fits = Duration::from_secs(3600);
assert!(
QueueRequest {
delay: Some(fits),
..QueueRequest::new("q")
}
.validate()
.is_ok()
);
assert!(
validate_start(
None,
Some(fits),
None,
WorkflowIdReusePolicy::ReturnExisting
)
.is_ok()
);
assert!(validate_start(None, None, None, WorkflowIdReusePolicy::ReturnExisting).is_ok());
let delay = QueueRequest {
delay: Some(Duration::MAX),
..QueueRequest::new("q")
}
.validate();
assert!(
matches!(&delay, Err(Error::InvalidArgument { operation, detail })
if operation == ENQUEUE_OPERATION
&& detail == "queue `q`: `delay` must fit in milliseconds as a 64-bit integer"),
"got {delay:?}"
);
assert!(
QueueRequest {
delay: Some(Duration::from_millis(i64::MAX as u64)),
..QueueRequest::new("q")
}
.validate()
.is_ok()
);
let both = validate_start(
Some(&QueueRequest {
delay: Some(Duration::ZERO),
..QueueRequest::new("q")
}),
Some(fits),
None,
WorkflowIdReusePolicy::ReturnExisting,
);
assert!(
matches!(&both, Err(Error::InvalidArgument { operation, detail })
if operation == ENQUEUE_OPERATION && detail.contains("`delay`")),
"got {both:?}"
);
let queued = validate_start(
Some(&QueueRequest::new("q")),
Some(Duration::MAX),
None,
WorkflowIdReusePolicy::ReturnExisting,
);
assert!(
matches!(&queued, Err(Error::InvalidArgument { operation, detail })
if operation == ENQUEUE_OPERATION
&& detail == "queue `q`: `timeout` must fit in milliseconds as a 64-bit integer"),
"got {queued:?}"
);
let timeout = validate_start(
None,
Some(Duration::MAX),
None,
WorkflowIdReusePolicy::ReturnExisting,
);
assert!(
matches!(&timeout, Err(Error::InvalidArgument { operation, detail })
if operation == START_OPERATION
&& detail == "`timeout` must fit in milliseconds as a 64-bit integer"),
"got {timeout:?}"
);
let delay = QueueRequest {
delay: Some(Duration::ZERO),
..QueueRequest::new("q")
}
.validate();
assert!(
matches!(&delay, Err(Error::InvalidArgument { operation, detail })
if operation == ENQUEUE_OPERATION
&& detail == "queue `q`: `delay` must be a positive, non-zero duration"),
"got {delay:?}"
);
let timeout = validate_start(
None,
Some(Duration::ZERO),
None,
WorkflowIdReusePolicy::ReturnExisting,
);
assert!(
matches!(&timeout, Err(Error::InvalidArgument { operation, detail })
if operation == START_OPERATION
&& detail == "`timeout` must be a positive, non-zero duration"),
"got {timeout:?}"
);
}
#[test]
fn the_recorded_start_error_names_match_the_errors_that_are_recorded() {
let errors = [
crate::sysdb::Error::QueueDeduplicated {
workflow_id: "w".to_owned(),
queue_name: "q".to_owned(),
deduplication_id: "d".to_owned(),
},
crate::sysdb::Error::ConflictingWorkflow {
workflow_id: "w".to_owned(),
detail: "d".to_owned(),
},
crate::sysdb::Error::WorkflowIdInUse {
workflow_id: "w".to_owned(),
status: crate::sysdb::types::WorkflowStatus::Success,
name: Some("n".to_owned()),
},
crate::sysdb::Error::InvalidInput {
field: "f".into(),
detail: "d".to_owned(),
},
crate::sysdb::Error::NonExistentWorkflow {
workflow_ids: vec!["w".to_owned()],
},
];
for error in &errors {
let value = serde_json::to_value(error).unwrap();
assert_eq!(
is_recorded_start_error(&value),
should_record_start_error(error),
"{value}"
);
}
let recorded = errors
.iter()
.filter(|error| should_record_start_error(error))
.count();
assert_eq!(recorded, RECORDED_START_ERRORS.len());
}
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 = crate::rt::spawn(async {
let _log = PanicLog {
workflow_id: "wf-boom",
};
crate::rt::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);
crate::rt::spawn(async move { tasks.abort_all().await })
};
assert!(
crate::rt::timeout(Duration::from_millis(100), &mut sweep)
.await
.is_err(),
"the sweep returned while a task was still live"
);
tasks.departed();
crate::rt::timeout(Duration::from_secs(10), sweep)
.await
.expect("the sweep never noticed the last task leaving")
.expect("the sweep panicked");
}
#[tokio::test]
async fn wait_all_waits_for_the_last_task() {
let tasks = Arc::new(Tasks::default());
tasks.arrived();
tasks.arrived();
let mut waiting = {
let tasks = Arc::clone(&tasks);
crate::rt::spawn(async move { tasks.wait_all().await })
};
tasks.departed();
assert!(
crate::rt::timeout(Duration::from_millis(100), &mut waiting)
.await
.is_err(),
"the wait ended with a task still live"
);
tasks.departed();
crate::rt::timeout(Duration::from_secs(10), waiting)
.await
.expect("the wait never noticed the last task leaving")
.expect("the wait panicked");
}
#[tokio::test]
async fn wait_all_until_gives_up_at_the_deadline_and_cancels_nothing() {
let tasks = Tasks::default();
tasks.arrived();
let task = crate::rt::spawn(std::future::pending::<()>());
tasks.insert(task.abort_handle(), "wf-slow".to_owned());
let deadline = Instant::now() + Duration::from_millis(100);
assert!(
!tasks.wait_all_until(deadline).await,
"a live task was reported ended"
);
assert!(!task.is_finished(), "the wait cancelled the task");
assert_eq!(tasks.running(), ["wf-slow"]);
task.abort();
tasks.departed();
assert!(tasks.wait_all_until(Instant::now()).await);
}
#[tokio::test]
async fn a_task_arriving_after_the_sweep_is_cancelled() {
let tasks = Tasks::default();
tasks.abort_all().await;
let task = crate::rt::spawn(std::future::pending::<()>());
tasks.insert(task.abort_handle(), "late".to_owned());
let joined = crate::rt::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)));
crate::rt::spawn(async {
let _panic_log = PanicLog {
workflow_id: "wf-fine",
};
crate::rt::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",
crate::context::TEST_OWNER,
None,
crate::serialization::PayloadEncoding::Native,
),
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",
crate::context::TEST_OWNER,
None,
crate::serialization::PayloadEncoding::Native,
);
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;
}
}