use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use crate::checkpoint::{Built, PendingStep, StepPlacement, StepWriter};
use crate::connection::Connection;
use crate::error::{DurableError, Error, Result};
use crate::handle::WorkflowHandle;
use crate::instance::{DBOS, Executor};
use crate::sysdb::types::{
AttributeUpdate, Fork as SysFork, ForkOptions as SysForkOptions,
RewindOptions as SysRewindOptions, StepRecord, WorkflowDelay, WorkflowFilter, WorkflowRecord,
duration_to_ms, step_names,
};
use crate::workflow::PendingStart;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Children {
#[default]
Skip,
Include,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResumeOptions<'a> {
pub queue_name: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fork<'a> {
pub original_id: &'a str,
pub start_step: i32,
pub forked_id: Option<&'a str>,
}
impl<'a> Fork<'a> {
pub fn new(original_id: &'a str, start_step: i32) -> Self {
Self {
original_id,
start_step,
forked_id: None,
}
}
fn validate(&self) -> Result<()> {
self.to_sys().validate().map_err(|error| {
let error = match error {
crate::sysdb::Error::InvalidInput { field, detail } if field == "start_step" => {
crate::sysdb::Error::InvalidInput {
detail: format!("{detail}, got {}", self.start_step),
field,
}
}
other => other,
};
refused_fork(error, |field| {
if self.original_id.is_empty() {
format!("Fork::{field}")
} else {
format!("Fork::{field} of `{}`", self.original_id)
}
})
})
}
fn to_sys(self) -> SysFork<'a> {
SysFork {
original_id: self.original_id,
forked_id: self.forked_id,
start_step: self.start_step,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ForkOptions<'a> {
pub app_version: Option<&'a str>,
pub queue_name: Option<&'a str>,
pub queue_partition_key: Option<&'a str>,
pub timeout: Option<Duration>,
pub replacement_children: &'a [(&'a str, &'a str)],
}
impl<'a> ForkOptions<'a> {
fn validate(&self) -> Result<()> {
self.to_sys().validate().map_err(|error| {
refused_fork(error, |field| {
let field = match field {
"application_version" => "app_version",
other => other,
};
format!("ForkOptions::{field}")
})
})
}
fn to_sys(self) -> SysForkOptions<'a> {
SysForkOptions {
application_version: self.app_version,
queue_name: self.queue_name,
queue_partition_key: self.queue_partition_key,
timeout: self.timeout,
replacement_children: self.replacement_children,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RewindOptions<'a> {
pub start_step: i32,
pub app_version: Option<&'a str>,
pub queue_name: Option<&'a str>,
pub queue_partition_key: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListWorkflowStepsOptions {
pub load_output: bool,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
impl Default for ListWorkflowStepsOptions {
fn default() -> Self {
Self {
load_output: true,
limit: None,
offset: None,
}
}
}
pub fn cancel_workflow<'a, E: DurableError + 'a>(workflow_id: &'a str) -> PendingStep<'a, (), E> {
pending_cancel(
StepPlacement::ambient_connection("cancel_workflow"),
workflow_id,
)
}
pub fn cancel_workflows<'a, E: DurableError + 'a>(
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, Vec<String>, E> {
pending_cancel_all(
StepPlacement::ambient_connection("cancel_workflows"),
workflow_ids,
children,
)
}
pub fn resume_workflow<'a, R: 'a, E: 'a>(workflow_id: &'a str) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_resume(
StepPlacement::ambient_connection("resume_workflow"),
workflow_id,
ResumeOptions::default(),
))
}
pub fn resume_workflow_with<'a, R: 'a, E: 'a>(
workflow_id: &'a str,
options: ResumeOptions<'a>,
) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_resume(
StepPlacement::ambient_connection("resume_workflow_with"),
workflow_id,
options,
))
}
pub fn resume_workflows<'a, R: 'a, E: 'a>(
workflow_ids: &'a [&'a str],
options: ResumeOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
pending_resume_all(
StepPlacement::ambient_connection("resume_workflows"),
workflow_ids,
options,
)
}
pub fn fork_workflow<'a, R: 'a, E: 'a>(
workflow_id: &'a str,
start_step: i32,
) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_fork(
StepPlacement::ambient_connection("fork_workflow"),
Fork::new(workflow_id, start_step),
ForkOptions::default(),
))
}
pub fn fork_workflow_with<'a, R: 'a, E: 'a>(
fork: Fork<'a>,
options: ForkOptions<'a>,
) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_fork(
StepPlacement::ambient_connection("fork_workflow_with"),
fork,
options,
))
}
pub fn fork_workflows<'a, R: 'a, E: 'a>(
forks: &'a [Fork<'a>],
options: ForkOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
pending_fork_all(
StepPlacement::ambient_connection("fork_workflows"),
forks,
options,
)
}
pub fn rewind_workflow<'a, R: 'a, E: 'a>(workflow_id: &'a str) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_rewind(
StepPlacement::ambient_connection("rewind_workflow"),
workflow_id,
RewindOptions::default(),
))
}
pub fn rewind_workflow_with<'a, R: 'a, E: 'a>(
workflow_id: &'a str,
options: RewindOptions<'a>,
) -> PendingStart<'a, R, E> {
PendingStart::from_step(pending_rewind(
StepPlacement::ambient_connection("rewind_workflow_with"),
workflow_id,
options,
))
}
pub fn delete_workflow<'a, E: DurableError + 'a>(workflow_id: &'a str) -> PendingStep<'a, (), E> {
pending_delete(
StepPlacement::ambient_connection("delete_workflow"),
workflow_id,
)
}
pub fn delete_workflows<'a, E: DurableError + 'a>(
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, u64, E> {
pending_delete_all(
StepPlacement::ambient_connection("delete_workflows"),
workflow_ids,
children,
)
}
pub fn set_workflow_delay<'a, E: DurableError + 'a>(
workflow_id: &'a str,
delay: WorkflowDelay,
) -> PendingStep<'a, (), E> {
pending_set_workflow_delay(
StepPlacement::ambient_connection("set_workflow_delay"),
workflow_id,
delay,
)
}
pub fn update_workflow_attributes<'a, E: DurableError + 'a>(
workflow_id: &'a str,
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> PendingStep<'a, (), E> {
pending_update_workflow_attributes(
StepPlacement::ambient_connection("update_workflow_attributes"),
workflow_id,
attributes,
)
}
pub fn merge_workflow_attributes<'a, E: DurableError + 'a>(
workflow_id: &'a str,
attributes: &serde_json::Map<String, serde_json::Value>,
) -> PendingStep<'a, (), E> {
pending_merge_workflow_attributes(
StepPlacement::ambient_connection("merge_workflow_attributes"),
workflow_id,
attributes,
)
}
pub fn list_workflows<'a, E: DurableError + 'a>(
filter: &'a WorkflowFilter<'a>,
) -> PendingStep<'a, Vec<WorkflowRecord>, E> {
pending_list_workflows(StepPlacement::ambient_connection("list_workflows"), filter)
}
pub fn list_workflow_steps<'a, E: DurableError + 'a>(
workflow_id: &'a str,
) -> PendingStep<'a, Vec<StepRecord>, E> {
pending_list_workflow_steps(
StepPlacement::ambient_connection("list_workflow_steps"),
workflow_id,
ListWorkflowStepsOptions::default(),
)
}
pub fn list_workflow_steps_with<'a, E: DurableError + 'a>(
workflow_id: &'a str,
options: ListWorkflowStepsOptions,
) -> PendingStep<'a, Vec<StepRecord>, E> {
pending_list_workflow_steps(
StepPlacement::ambient_connection("list_workflow_steps_with"),
workflow_id,
options,
)
}
pub fn retrieve_workflow<R, E: DurableError>(workflow_id: &str) -> Result<WorkflowHandle<R, E>, E> {
let conn = StepPlacement::ambient_connection("retrieve_workflow").map_err(Error::lift)?;
Ok(retrieved(conn, workflow_id))
}
impl DBOS {
pub(crate) fn placed(&self, operation: &'static str) -> Result<(Arc<Executor>, StepPlacement)> {
StepPlacement::taken(self.executor(operation), operation)
}
fn connection(&self, operation: &'static str) -> Result<Arc<Connection>> {
Ok(Arc::clone(self.executor(operation)?.connection()))
}
pub fn retrieve_workflow<R, E>(&self, workflow_id: &str) -> Result<WorkflowHandle<R, E>> {
Ok(retrieved(
Arc::clone(self.executor("retrieve a workflow")?.connection()),
workflow_id,
))
}
pub fn cancel<'a>(&self, workflow_id: &'a str) -> PendingStep<'a, ()> {
pending_cancel(self.connection("cancel a workflow"), workflow_id)
}
pub fn cancel_all<'a>(
&self,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, Vec<String>> {
pending_cancel_all(self.connection("cancel a workflow"), workflow_ids, children)
}
pub fn resume<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
self.resume_with(workflow_id, ResumeOptions::default())
}
pub fn resume_with<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
options: ResumeOptions<'a>,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
PendingStart::from_step(pending_resume(
self.connection("resume a workflow"),
workflow_id,
options,
))
}
pub fn resume_all<'a, R: 'a, E: 'a>(
&self,
workflow_ids: &'a [&'a str],
options: ResumeOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
pending_resume_all(self.connection("resume a workflow"), workflow_ids, options)
}
pub fn fork<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
start_step: i32,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
self.fork_with(Fork::new(workflow_id, start_step), ForkOptions::default())
}
pub fn fork_with<'a, R: 'a, E: 'a>(
&self,
fork: Fork<'a>,
options: ForkOptions<'a>,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
PendingStart::from_step(pending_fork(self.connection(FORK_OPERATION), fork, options))
}
pub fn fork_all<'a, R: 'a, E: 'a>(
&self,
forks: &'a [Fork<'a>],
options: ForkOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
pending_fork_all(self.connection(FORK_OPERATION), forks, options)
}
pub fn rewind<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
self.rewind_with(workflow_id, RewindOptions::default())
}
pub fn rewind_with<'a, R: 'a, E: 'a>(
&self,
workflow_id: &'a str,
options: RewindOptions<'a>,
) -> PendingStart<'a, R, E, crate::EngineOnly> {
PendingStart::from_step(pending_rewind(
self.connection("rewind a workflow"),
workflow_id,
options,
))
}
pub fn delete<'a>(&self, workflow_id: &'a str) -> PendingStep<'a, ()> {
pending_delete(self.connection("delete a workflow"), workflow_id)
}
pub fn delete_all<'a>(
&self,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, u64> {
pending_delete_all(self.connection("delete a workflow"), workflow_ids, children)
}
pub fn set_workflow_delay<'a>(
&self,
workflow_id: &'a str,
delay: WorkflowDelay,
) -> PendingStep<'a, ()> {
pending_set_workflow_delay(self.connection(DELAY_OPERATION), workflow_id, delay)
}
pub fn update_workflow_attributes<'a>(
&self,
workflow_id: &'a str,
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> PendingStep<'a, ()> {
pending_update_workflow_attributes(
self.connection("update a workflow's attributes"),
workflow_id,
attributes,
)
}
pub fn merge_workflow_attributes<'a>(
&self,
workflow_id: &'a str,
attributes: &serde_json::Map<String, serde_json::Value>,
) -> PendingStep<'a, ()> {
pending_merge_workflow_attributes(
self.connection("merge a workflow's attributes"),
workflow_id,
attributes,
)
}
pub fn list_workflows<'a>(
&self,
filter: &'a WorkflowFilter<'a>,
) -> PendingStep<'a, Vec<WorkflowRecord>> {
pending_list_workflows(self.connection("list workflows"), filter)
}
pub fn list_workflow_steps<'a>(
&self,
workflow_id: &'a str,
) -> PendingStep<'a, Vec<StepRecord>> {
self.list_workflow_steps_with(workflow_id, ListWorkflowStepsOptions::default())
}
pub fn list_workflow_steps_with<'a>(
&self,
workflow_id: &'a str,
options: ListWorkflowStepsOptions,
) -> PendingStep<'a, Vec<StepRecord>> {
pending_list_workflow_steps(
self.connection("list a workflow's steps"),
workflow_id,
options,
)
}
}
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, 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, None)
.await
}
pub async fn fork<R, E>(
&self,
workflow_id: &str,
start_step: i32,
) -> Result<WorkflowHandle<R, E>> {
self.fork_with(Fork::new(workflow_id, start_step), ForkOptions::default())
.await
}
pub async fn fork_with<R, E>(
&self,
fork: Fork<'_>,
options: ForkOptions<'_>,
) -> Result<WorkflowHandle<R, E>> {
refuse_invalid_forks(std::slice::from_ref(&fork), &options)?;
self.connection()
.fork_one(fork.to_sys(), &options.to_sys(), None)
.await
}
pub async fn fork_all<R, E>(
&self,
forks: &[Fork<'_>],
options: ForkOptions<'_>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
refuse_invalid_forks(forks, &options)?;
let forks: Vec<SysFork<'_>> = forks.iter().map(|fork| fork.to_sys()).collect();
self.connection()
.fork_all(&forks, &options.to_sys(), None)
.await
}
pub async fn rewind<R, E>(&self, workflow_id: &str) -> Result<WorkflowHandle<R, E>> {
self.rewind_with(workflow_id, RewindOptions::default())
.await
}
pub async fn rewind_with<R, E>(
&self,
workflow_id: &str,
options: RewindOptions<'_>,
) -> Result<WorkflowHandle<R, E>> {
refuse_invalid_rewind_options(&options)?;
self.connection().rewind(workflow_id, 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,
AttributeUpdate::Replace(encoded.as_deref()),
None,
)
.await
}
pub async fn merge_workflow_attributes(
&self,
workflow_id: &str,
attributes: &serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
let encoded = crate::workflow::encode_attribute_map(attributes)?;
self.connection()
.update_workflow_attributes(workflow_id, AttributeUpdate::Merge(&encoded), 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.list_workflow_steps_with(workflow_id, ListWorkflowStepsOptions::default())
.await
}
pub async fn list_workflow_steps_with(
&self,
workflow_id: &str,
options: ListWorkflowStepsOptions,
) -> Result<Vec<StepRecord>> {
self.connection()
.list_workflow_steps(workflow_id, options, None)
.await
}
}
impl Connection {
pub(crate) async fn cancel_all(
&self,
workflow_ids: &[&str],
children: Children,
from_workflow: Option<&str>,
caller: Option<StepWriter<'_>>,
) -> Result<Vec<String>> {
let cancelled = self
.sysdb()
.cancel_workflows(
workflow_ids,
children == Children::Include,
from_workflow,
self.step_caller(caller),
)
.await
.map_err(Error::from_sysdb)?;
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<'_>,
from_workflow: Option<&str>,
caller: Option<StepWriter<'_>>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
let resumed = self
.sysdb()
.resume_workflows(
workflow_ids,
options.queue_name,
from_workflow,
self.step_caller(caller),
)
.await
.map_err(Error::from_sysdb)?;
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>,
forks: &[SysFork<'_>],
options: &SysForkOptions<'_>,
caller: Option<StepWriter<'_>>,
) -> Result<Vec<WorkflowHandle<R, E>>> {
let forked = self
.sysdb()
.fork_workflows(forks, options, self.step_caller(caller))
.await
.map_err(Error::from_sysdb)?;
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 fork_one<R, E>(
self: &Arc<Self>,
fork: SysFork<'_>,
options: &SysForkOptions<'_>,
caller: Option<StepWriter<'_>>,
) -> Result<WorkflowHandle<R, E>> {
self.fork_all(std::slice::from_ref(&fork), options, caller)
.await?
.pop()
.ok_or_else(|| {
Error::Config(format!(
"forking `{}` produced no workflow",
fork.original_id
))
})
}
pub(crate) async fn rewind<R, E>(
self: &Arc<Self>,
workflow_id: &str,
options: RewindOptions<'_>,
caller: Option<StepWriter<'_>>,
) -> Result<WorkflowHandle<R, E>> {
let sys_options = SysRewindOptions {
application_version: options.app_version,
queue_name: options.queue_name,
queue_partition_key: options.queue_partition_key,
};
self.sysdb()
.rewind_workflow(
workflow_id,
options.start_step,
&sys_options,
self.step_caller(caller),
)
.await
.map_err(Error::from_sysdb)?;
tracing::info!(
workflow_id,
start_step = options.start_step,
"rewound the workflow onto its queue"
);
Ok(WorkflowHandle::polling(
Arc::clone(self),
workflow_id.to_owned(),
true,
))
}
pub(crate) async fn delete_all(
&self,
workflow_ids: &[&str],
children: Children,
caller: Option<StepWriter<'_>>,
) -> Result<u64> {
let deleted = self
.sysdb()
.delete_workflows(
workflow_ids,
children == Children::Include,
self.step_caller(caller),
)
.await
.map_err(Error::from_sysdb)?;
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<StepWriter<'_>>,
) -> Result<()> {
self.sysdb()
.set_workflow_delay(workflow_id, delay, self.step_caller(caller))
.await
.map_err(|error| Error::refused_argument(DELAY_OPERATION, error))?;
tracing::info!(workflow_id, "asked to move the workflow's release time");
Ok(())
}
pub(crate) async fn update_workflow_attributes(
&self,
workflow_id: &str,
update: AttributeUpdate<'_>,
caller: Option<StepWriter<'_>>,
) -> Result<()> {
self.sysdb()
.update_workflow_attributes(workflow_id, update, self.step_caller(caller))
.await
.map_err(Error::from_sysdb)?;
let verb = match update {
AttributeUpdate::Replace(_) => "replace",
AttributeUpdate::Merge(_) => "merge into",
};
tracing::info!(workflow_id, "asked to {verb} the workflow's attributes");
Ok(())
}
pub(crate) async fn list_workflows(
&self,
filter: &WorkflowFilter<'_>,
caller: Option<StepWriter<'_>>,
) -> Result<Vec<WorkflowRecord>> {
self.sysdb()
.list_workflows(filter, self.step_caller(caller))
.await
.map_err(Error::from_sysdb)
}
pub(crate) async fn list_workflow_steps(
&self,
workflow_id: &str,
options: ListWorkflowStepsOptions,
caller: Option<StepWriter<'_>>,
) -> Result<Vec<StepRecord>> {
self.sysdb()
.list_workflow_steps(
workflow_id,
options.load_output,
options.limit,
options.offset,
self.step_caller(caller),
)
.await
.map_err(Error::from_sysdb)
}
}
fn place(conn: Result<Arc<Connection>>, operation: &'static str) -> Built<Arc<Connection>> {
conn.and_then(|conn| Connection::placed(&conn, operation))
}
fn place_after<C>(
conn: Result<Arc<Connection>>,
operation: &'static str,
check: impl FnOnce() -> Result<C>,
) -> Built<(Arc<Connection>, C)> {
let conn = conn?;
let checked = check()?;
let placement = StepPlacement::of(&conn, operation)?;
Ok(((conn, checked), placement))
}
fn retrieved<R, E>(conn: Arc<Connection>, workflow_id: &str) -> WorkflowHandle<R, E> {
WorkflowHandle::polling(conn, workflow_id.to_owned(), false)
}
fn pending_cancel<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
) -> PendingStep<'a, (), E> {
PendingStep::placed(
step_names::CANCEL_WORKFLOW,
place(conn, "cancel a workflow"),
move |conn, placement| async move {
conn.cancel_all(
&[workflow_id],
Children::Skip,
placement.workflow_id(),
placement.step_writer(),
)
.await
.map_err(Error::lift)?;
Ok(())
},
)
}
fn pending_cancel_all<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, Vec<String>, E> {
PendingStep::placed(
step_names::CANCEL_WORKFLOW,
place(conn, "cancel a workflow"),
move |conn, placement| async move {
conn.cancel_all(
workflow_ids,
children,
placement.workflow_id(),
placement.step_writer(),
)
.await
.map_err(Error::lift)
},
)
}
fn pending_resume<'a, R: 'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
options: ResumeOptions<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
PendingStep::placed(
step_names::RESUME_WORKFLOW,
place(conn, "resume a workflow"),
move |conn, placement| async move {
conn.resume_all(
&[workflow_id],
options,
placement.workflow_id(),
placement.step_writer(),
)
.await
.map_err(Error::lift)?
.pop()
.ok_or_else(|| Error::Config(format!("resuming `{workflow_id}` produced no handle")))
},
)
}
fn pending_resume_all<'a, R: 'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_ids: &'a [&'a str],
options: ResumeOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
PendingStep::placed(
step_names::RESUME_WORKFLOW,
place(conn, "resume a workflow"),
move |conn, placement| async move {
conn.resume_all(
workflow_ids,
options,
placement.workflow_id(),
placement.step_writer(),
)
.await
.map_err(Error::lift)
},
)
}
fn pending_fork<'a, R: 'a, E: 'a>(
conn: Result<Arc<Connection>>,
fork: Fork<'a>,
options: ForkOptions<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
let built = place_after(conn, FORK_OPERATION, || {
refuse_invalid_forks(std::slice::from_ref(&fork), &options)
})
.map(|((conn, ()), placement)| (conn, placement));
PendingStep::placed(
step_names::FORK_WORKFLOW,
built,
move |conn, placement| async move {
conn.fork_one(fork.to_sys(), &options.to_sys(), placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_fork_all<'a, R: 'a, E: 'a>(
conn: Result<Arc<Connection>>,
forks: &'a [Fork<'a>],
options: ForkOptions<'a>,
) -> PendingStep<'a, Vec<WorkflowHandle<R, E>>> {
let built = place_after(conn, FORK_OPERATION, || {
refuse_invalid_forks(forks, &options)
})
.map(|((conn, ()), placement)| (conn, placement));
PendingStep::placed(
step_names::FORK_WORKFLOW,
built,
move |conn, placement| async move {
let forks: Vec<SysFork<'_>> = forks.iter().map(|fork| fork.to_sys()).collect();
conn.fork_all(&forks, &options.to_sys(), placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_rewind<'a, R: 'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
options: RewindOptions<'a>,
) -> PendingStep<'a, WorkflowHandle<R, E>> {
let built = place_after(conn, "rewind a workflow", || {
refuse_invalid_rewind_options(&options)
})
.map(|((conn, ()), placement)| (conn, placement));
PendingStep::placed(
step_names::REWIND_WORKFLOW,
built,
move |conn, placement| async move {
conn.rewind(workflow_id, options, placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_delete<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
) -> PendingStep<'a, (), E> {
PendingStep::placed(
step_names::DELETE_WORKFLOW,
place(conn, "delete a workflow"),
move |conn, placement| async move {
conn.delete_all(&[workflow_id], Children::Skip, placement.step_writer())
.await
.map_err(Error::lift)?;
Ok(())
},
)
}
fn pending_delete_all<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_ids: &'a [&'a str],
children: Children,
) -> PendingStep<'a, u64, E> {
PendingStep::placed(
step_names::DELETE_WORKFLOW,
place(conn, "delete a workflow"),
move |conn, placement| async move {
conn.delete_all(workflow_ids, children, placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_set_workflow_delay<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
delay: WorkflowDelay,
) -> PendingStep<'a, (), E> {
let built = place_after(conn, DELAY_OPERATION, || refuse_unstorable_delay(delay))
.map(|((conn, ()), placement)| (conn, placement));
PendingStep::placed(
step_names::SET_WORKFLOW_DELAY,
built,
move |conn, placement| async move {
conn.set_workflow_delay(workflow_id, delay, placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_update_workflow_attributes<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
attributes: Option<&serde_json::Map<String, serde_json::Value>>,
) -> PendingStep<'a, (), E> {
PendingStep::placed(
step_names::UPDATE_WORKFLOW_ATTRIBUTES,
place_after(conn, "update a workflow's attributes", || {
crate::workflow::encode_attributes(attributes)
}),
move |(conn, encoded), placement| async move {
conn.update_workflow_attributes(
workflow_id,
AttributeUpdate::Replace(encoded.as_deref()),
placement.step_writer(),
)
.await
.map_err(Error::lift)
},
)
}
fn pending_merge_workflow_attributes<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
attributes: &serde_json::Map<String, serde_json::Value>,
) -> PendingStep<'a, (), E> {
PendingStep::placed(
step_names::UPDATE_WORKFLOW_ATTRIBUTES,
place_after(conn, "merge a workflow's attributes", || {
crate::workflow::encode_attribute_map(attributes)
}),
move |(conn, encoded), placement| async move {
conn.update_workflow_attributes(
workflow_id,
AttributeUpdate::Merge(&encoded),
placement.step_writer(),
)
.await
.map_err(Error::lift)
},
)
}
fn pending_list_workflows<'a, E: 'a>(
conn: Result<Arc<Connection>>,
filter: &'a WorkflowFilter<'a>,
) -> PendingStep<'a, Vec<WorkflowRecord>, E> {
PendingStep::placed(
step_names::LIST_WORKFLOWS,
place(conn, "list workflows"),
move |conn, placement| async move {
conn.list_workflows(filter, placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn pending_list_workflow_steps<'a, E: 'a>(
conn: Result<Arc<Connection>>,
workflow_id: &'a str,
options: ListWorkflowStepsOptions,
) -> PendingStep<'a, Vec<StepRecord>, E> {
PendingStep::placed(
step_names::LIST_WORKFLOW_STEPS,
place(conn, "list a workflow's steps"),
move |conn, placement| async move {
conn.list_workflow_steps(workflow_id, options, placement.step_writer())
.await
.map_err(Error::lift)
},
)
}
fn refuse_negative_start_step(operation: &'static str, field: &str, start_step: i32) -> Result<()> {
if start_step < 0 {
return Err(Error::InvalidArgument {
operation: operation.into(),
detail: format!("{field} must not be negative, got {start_step}"),
});
}
Ok(())
}
const DELAY_OPERATION: &str = "delay a workflow";
fn refuse_unstorable_delay(delay: WorkflowDelay) -> Result<()> {
match delay {
WorkflowDelay::For(duration) => duration_to_ms("delay", duration)
.map(drop)
.map_err(|error| Error::refused_argument(DELAY_OPERATION, error)),
WorkflowDelay::Until(_) => Ok(()),
}
}
const FORK_OPERATION: &str = "fork a workflow";
fn refuse_invalid_forks(forks: &[Fork<'_>], options: &ForkOptions<'_>) -> Result<()> {
let refuse = |detail: String| {
Err(Error::InvalidArgument {
operation: FORK_OPERATION.into(),
detail,
})
};
let original_ids: HashSet<&str> = forks.iter().map(|fork| fork.original_id).collect();
let mut forked_ids = HashSet::with_capacity(forks.len());
for fork in forks {
fork.validate()?;
if let Some(id) = fork.forked_id {
if original_ids.contains(id) {
return refuse(format!("`{id}` is both a forked id and an original id"));
}
if !forked_ids.insert(id) {
return refuse(format!("`{id}` is the forked id of more than one fork"));
}
}
}
options.validate()
}
fn refused_fork(error: crate::sysdb::Error, field: impl FnOnce(&str) -> String) -> Error {
Error::from_refusal(error, |name, detail| Error::InvalidArgument {
operation: FORK_OPERATION.into(),
detail: format!("{}: {detail}", field(name)),
})
}
fn refuse_invalid_rewind_options(options: &RewindOptions<'_>) -> Result<()> {
refuse_negative_start_step(
"rewind a workflow",
"RewindOptions::start_step",
options.start_step,
)?;
for (field, value) in [
("app_version", options.app_version),
("queue_name", options.queue_name),
("queue_partition_key", options.queue_partition_key),
] {
if value == Some("") {
return Err(Error::InvalidArgument {
operation: "rewind a workflow".into(),
detail: format!("RewindOptions::{field} must be absent rather than empty"),
});
}
}
Ok(())
}