use std::sync::Arc;
use std::time::Duration;
use crate::checkpoint::{PendingStep, StepPlacement};
use crate::connection::Connection;
use crate::error::{Error, Result};
use crate::handle::WorkflowHandle;
use crate::instance::{DBOS, Executor};
use crate::sysdb::types::{
Fork, ForkOptions as SysForkOptions, ForkPoint, StepRecord, WorkflowDelay, WorkflowFilter,
WorkflowRecord, step_names,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Children {
#[default]
Skip,
Include,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ForkFrom<'a> {
#[default]
Beginning,
Step(i32),
LastFailure,
LastStep,
StepNamed(&'a str),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResumeOptions<'a> {
pub queue: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ForkOptions<'a> {
pub forked_id: Option<&'a str>,
pub app_version: Option<&'a str>,
pub queue: Option<&'a str>,
pub queue_partition_key: Option<&'a str>,
pub timeout: Option<Duration>,
}
impl DBOS {
fn placed(&self, operation: &'static str) -> Result<(Arc<Executor>, StepPlacement)> {
StepPlacement::taken(self.executor(operation), operation)
}
pub fn retrieve_workflow<R, E>(&self, workflow_id: &str) -> Result<WorkflowHandle<R, E>> {
let executor = self.executor("retrieve a workflow")?;
Ok(WorkflowHandle::polling(
Arc::clone(executor.connection()),
workflow_id.to_owned(),
false,
))
}
pub fn cancel<'a>(&self, workflow_id: &'a str) -> PendingStep<'a, ()> {
PendingStep::placed(
step_names::CANCEL_WORKFLOW,
self.placed("cancel a workflow"),
move |executor, placement| async move {
executor
.connection()
.cancel_all(&[workflow_id], Children::Skip, placement.step())
.await?;
Ok(())
},
)
}
pub fn cancel_all<'a>(
&self,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, Vec<String>> {
PendingStep::placed(
step_names::CANCEL_WORKFLOW,
self.placed("cancel a workflow"),
move |executor, placement| async move {
executor
.connection()
.cancel_all(workflow_ids, children, placement.step())
.await
},
)
}
pub fn resume<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
self.resume_with(workflow_id, ResumeOptions::default())
}
pub fn resume_with<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
options: ResumeOptions<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
PendingStep::placed(
step_names::RESUME_WORKFLOW,
self.placed("resume a workflow"),
move |executor, placement| async move {
executor
.connection()
.resume_all(&[workflow_id], options, placement.step())
.await?
.pop()
.ok_or_else(|| {
Error::Config(format!("resuming `{workflow_id}` produced no handle"))
})
},
)
}
pub fn resume_all<'a, R: 'a, E: 'a>(
&self,
workflow_ids: &'a [&'a str],
options: ResumeOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
PendingStep::placed(
step_names::RESUME_WORKFLOW,
self.placed("resume a workflow"),
move |executor, placement| async move {
executor
.connection()
.resume_all(workflow_ids, options, placement.step())
.await
},
)
}
pub fn fork<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
from: ForkFrom<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
self.fork_with(workflow_id, from, ForkOptions::default())
}
pub fn fork_with<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
from: ForkFrom<'a>,
options: ForkOptions<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
let built = self.executor("fork a workflow").and_then(|executor| {
refuse_chosen_id_without_a_step(from, options.forked_id)?;
let placement = StepPlacement::of(executor.connection(), "fork a workflow")?;
Ok((executor, placement))
});
PendingStep::placed(
step_names::FORK_WORKFLOW,
built,
move |executor, placement| async move {
executor
.connection()
.fork_all(
&[workflow_id],
from,
options.forked_id,
&options,
placement.step(),
)
.await?
.pop()
.ok_or_else(|| {
Error::Config(format!("forking `{workflow_id}` produced no workflow"))
})
},
)
}
pub fn fork_all<'a, R: 'a, E: 'a>(
&self,
workflow_ids: &'a [&'a str],
from: ForkFrom<'a>,
options: ForkOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
let built = self.executor("fork a workflow").and_then(|executor| {
refuse_forked_id_in_bulk(&options)?;
let placement = StepPlacement::of(executor.connection(), "fork a workflow")?;
Ok((executor, placement))
});
PendingStep::placed(
step_names::FORK_WORKFLOW,
built,
move |executor, placement| async move {
executor
.connection()
.fork_all(workflow_ids, from, None, &options, placement.step())
.await
},
)
}
pub fn delete<'a>(&self, workflow_id: &'a str) -> PendingStep<'a, ()> {
PendingStep::placed(
step_names::DELETE_WORKFLOW,
self.placed("delete a workflow"),
move |executor, placement| async move {
executor
.connection()
.delete_all(&[workflow_id], Children::Skip, placement.step())
.await?;
Ok(())
},
)
}
pub fn delete_all<'a>(
&self,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, u64> {
PendingStep::placed(
step_names::DELETE_WORKFLOW,
self.placed("delete a workflow"),
move |executor, placement| async move {
executor
.connection()
.delete_all(workflow_ids, children, placement.step())
.await
},
)
}
pub fn set_workflow_delay<'a>(
&self,
workflow_id: &'a str,
delay: WorkflowDelay,
) -> PendingStep<'a, ()> {
PendingStep::placed(
step_names::SET_WORKFLOW_DELAY,
self.placed("delay a workflow"),
move |executor, placement| async move {
executor
.connection()
.set_workflow_delay(workflow_id, delay, placement.step())
.await
},
)
}
pub fn update_workflow_attributes<'a>(
&self,
workflow_id: &'a str,
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> PendingStep<'a, ()> {
let built = crate::workflow::encode_attributes(attributes)
.and_then(|encoded| Ok((encoded, self.placed("update a workflow's attributes")?)))
.map(|(encoded, (executor, placement))| ((executor, encoded), placement));
PendingStep::placed(
step_names::UPDATE_WORKFLOW_ATTRIBUTES,
built,
move |(executor, encoded), placement| async move {
executor
.connection()
.update_workflow_attributes(workflow_id, encoded.as_deref(), placement.step())
.await
},
)
}
pub fn list_workflows<'a>(
&self,
filter: &'a WorkflowFilter<'a>,
) -> PendingStep<'a, Vec<WorkflowRecord>> {
PendingStep::placed(
step_names::LIST_WORKFLOWS,
self.placed("list workflows"),
move |executor, placement| async move {
executor
.connection()
.list_workflows(filter, placement.step())
.await
},
)
}
pub fn list_workflow_steps<'a>(
&self,
workflow_id: &'a str,
) -> PendingStep<'a, Vec<StepRecord>> {
PendingStep::placed(
step_names::LIST_WORKFLOW_STEPS,
self.placed("list a workflow's steps"),
move |executor, placement| async move {
executor
.connection()
.list_workflow_steps(workflow_id, placement.step())
.await
},
)
}
}
impl crate::Client {
pub async fn cancel(&self, workflow_id: &str) -> Result<()> {
self.cancel_all(&[workflow_id], Children::Skip).await?;
Ok(())
}
pub async fn cancel_all(
&self,
workflow_ids: &[&str],
children: Children,
) -> Result<Vec<String>> {
self.connection()
.cancel_all(workflow_ids, children, None)
.await
}
pub async fn resume<R, E>(&self, workflow_id: &str) -> Result<WorkflowHandle<R, E>> {
self.resume_with(workflow_id, ResumeOptions::default())
.await
}
pub async fn resume_with<R, E>(
&self,
workflow_id: &str,
options: ResumeOptions<'_>,
) -> Result<WorkflowHandle<R, E>> {
self.resume_all(&[workflow_id], options)
.await?
.pop()
.ok_or_else(|| Error::Config(format!("resuming `{workflow_id}` produced no handle")))
}
pub async fn resume_all<R, E>(
&self,
workflow_ids: &[&str],
options: ResumeOptions<'_>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
self.connection()
.resume_all(workflow_ids, options, None)
.await
}
pub async fn fork<R, E>(
&self,
workflow_id: &str,
from: ForkFrom<'_>,
) -> Result<WorkflowHandle<R, E>> {
self.fork_with(workflow_id, from, ForkOptions::default())
.await
}
pub async fn fork_with<R, E>(
&self,
workflow_id: &str,
from: ForkFrom<'_>,
options: ForkOptions<'_>,
) -> Result<WorkflowHandle<R, E>> {
refuse_chosen_id_without_a_step(from, options.forked_id)?;
self.connection()
.fork_all(&[workflow_id], from, options.forked_id, &options, None)
.await?
.pop()
.ok_or_else(|| Error::Config(format!("forking `{workflow_id}` produced no workflow")))
}
pub async fn fork_all<R, E>(
&self,
workflow_ids: &[&str],
from: ForkFrom<'_>,
options: ForkOptions<'_>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
refuse_forked_id_in_bulk(&options)?;
self.connection()
.fork_all(workflow_ids, from, None, &options, None)
.await
}
pub async fn delete(&self, workflow_id: &str) -> Result<()> {
self.delete_all(&[workflow_id], Children::Skip).await?;
Ok(())
}
pub async fn delete_all(&self, workflow_ids: &[&str], children: Children) -> Result<u64> {
self.connection()
.delete_all(workflow_ids, children, None)
.await
}
pub async fn set_workflow_delay(&self, workflow_id: &str, delay: WorkflowDelay) -> Result<()> {
self.connection()
.set_workflow_delay(workflow_id, delay, None)
.await
}
pub async fn update_workflow_attributes(
&self,
workflow_id: &str,
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Result<()> {
let encoded = crate::workflow::encode_attributes(attributes)?;
self.connection()
.update_workflow_attributes(workflow_id, encoded.as_deref(), None)
.await
}
pub async fn list_workflows(&self, filter: &WorkflowFilter<'_>) -> Result<Vec<WorkflowRecord>> {
self.connection().list_workflows(filter, None).await
}
pub async fn list_workflow_steps(&self, workflow_id: &str) -> Result<Vec<StepRecord>> {
self.connection()
.list_workflow_steps(workflow_id, None)
.await
}
}
impl Connection {
pub(crate) async fn cancel_all(
&self,
workflow_ids: &[&str],
children: Children,
caller: Option<(&str, i32)>,
) -> Result<Vec<String>> {
let cancelled = self
.sysdb()
.cancel_workflows(workflow_ids, children == Children::Include, caller)
.await
.map_err(Error::SystemDatabase)?;
if !cancelled.is_empty() {
tracing::info!(cancelled = cancelled.len(), "cancelled workflows");
}
Ok(cancelled)
}
pub(crate) async fn resume_all<R, E>(
self: &Arc<Self>,
workflow_ids: &[&str],
options: ResumeOptions<'_>,
caller: Option<(&str, i32)>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
let resumed = self
.sysdb()
.resume_workflows(workflow_ids, options.queue, caller)
.await
.map_err(Error::SystemDatabase)?;
tracing::info!(
requested = workflow_ids.len(),
resumed = resumed.len(),
"resumed workflows onto their queues"
);
Ok(workflow_ids
.iter()
.map(|id| WorkflowHandle::polling(Arc::clone(self), (*id).to_owned(), true))
.collect())
}
pub(crate) async fn fork_all<R, E>(
self: &Arc<Self>,
workflow_ids: &[&str],
from: ForkFrom<'_>,
forked_id: Option<&str>,
options: &ForkOptions<'_>,
caller: Option<(&str, i32)>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
let sys_options = SysForkOptions {
application_version: options.app_version,
queue_name: options.queue,
queue_partition_key: options.queue_partition_key,
timeout: options.timeout,
replacement_children: &[],
};
let forked = match from {
ForkFrom::Beginning | ForkFrom::Step(_) => {
let start_step = match from {
ForkFrom::Step(step) => step,
_ => 0,
};
let forks: Vec<Fork<'_>> = workflow_ids
.iter()
.map(|source_id| Fork {
source_id,
forked_id,
start_step,
})
.collect();
self.sysdb()
.fork_workflows(&forks, &sys_options, caller)
.await
}
ForkFrom::LastFailure => {
self.sysdb()
.fork_from(workflow_ids, ForkPoint::LastFailure, &sys_options, caller)
.await
}
ForkFrom::LastStep => {
self.sysdb()
.fork_from(workflow_ids, ForkPoint::LastStep, &sys_options, caller)
.await
}
ForkFrom::StepNamed(name) => {
self.sysdb()
.fork_from(
workflow_ids,
ForkPoint::StepNamed(name),
&sys_options,
caller,
)
.await
}
}
.map_err(Error::SystemDatabase)?;
match forked.as_slice() {
[only] => tracing::info!(forked_id = only, "forked the workflow onto its queue"),
many => tracing::info!(count = many.len(), "forked workflows onto their queues"),
}
Ok(forked
.into_iter()
.map(|id| WorkflowHandle::polling(Arc::clone(self), id, true))
.collect())
}
pub(crate) async fn delete_all(
&self,
workflow_ids: &[&str],
children: Children,
caller: Option<(&str, i32)>,
) -> Result<u64> {
let deleted = self
.sysdb()
.delete_workflows(workflow_ids, children == Children::Include, caller)
.await
.map_err(Error::SystemDatabase)?;
if deleted > 0 {
tracing::info!(deleted, "deleted workflows");
}
Ok(deleted)
}
pub(crate) async fn set_workflow_delay(
&self,
workflow_id: &str,
delay: WorkflowDelay,
caller: Option<(&str, i32)>,
) -> Result<()> {
self.sysdb()
.set_workflow_delay(workflow_id, delay, caller)
.await
.map_err(Error::SystemDatabase)?;
tracing::info!(workflow_id, "asked to move the workflow's release time");
Ok(())
}
pub(crate) async fn update_workflow_attributes(
&self,
workflow_id: &str,
encoded: Option<&str>,
caller: Option<(&str, i32)>,
) -> Result<()> {
self.sysdb()
.update_workflow_attributes(workflow_id, encoded, caller)
.await
.map_err(Error::SystemDatabase)?;
tracing::info!(workflow_id, "asked to replace the workflow's attributes");
Ok(())
}
pub(crate) async fn list_workflows(
&self,
filter: &WorkflowFilter<'_>,
caller: Option<(&str, i32)>,
) -> Result<Vec<WorkflowRecord>> {
self.sysdb()
.list_workflows(filter, caller)
.await
.map_err(Error::SystemDatabase)
}
pub(crate) async fn list_workflow_steps(
&self,
workflow_id: &str,
caller: Option<(&str, i32)>,
) -> Result<Vec<StepRecord>> {
self.sysdb()
.list_workflow_steps(workflow_id, true, None, None, caller)
.await
.map_err(Error::SystemDatabase)
}
}
fn refuse_chosen_id_without_a_step(from: ForkFrom<'_>, forked_id: Option<&str>) -> Result<()> {
if forked_id.is_some() && !matches!(from, ForkFrom::Beginning | ForkFrom::Step(_)) {
return Err(Error::InvalidArgument {
operation: "fork a workflow".into(),
detail: "ForkOptions::forked_id needs a fork point that names its step: use \
ForkFrom::Step, or ForkFrom::Beginning, and let the searched fork points \
generate the id"
.to_owned(),
});
}
Ok(())
}
fn refuse_forked_id_in_bulk(options: &ForkOptions<'_>) -> Result<()> {
if options.forked_id.is_some() {
return Err(Error::InvalidArgument {
operation: "fork_all".into(),
detail: "ForkOptions::forked_id names a single fork and cannot be used with fork_all"
.to_owned(),
});
}
Ok(())
}