use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use dbos::sysdb::SystemDatabase;
use dbos::sysdb::postgres::{PostgresSystemDatabase, Settings};
use dbos::sysdb::types::{Outcome, WorkflowStatus};
use dbos::{
Config, DBOS, Debouncer, DuplicationPolicy, Error, QueueConflict, QueueOptions, QueueRequest,
RunOptions, StartOptions, Timeout, WorkflowIdReusePolicy,
};
use dbos_test_support::{Backend, TestDatabase, owner_token, test_database};
fn app_version(app_name: &str) -> String {
format!("{app_name}-1.0.0")
}
fn config(app_name: &str, db: &TestDatabase) -> Config {
Config {
migrate: false,
app_version: Some(app_version(app_name)),
..Config::new(app_name, db.url())
}
}
async fn reader(db: &TestDatabase) -> PostgresSystemDatabase {
PostgresSystemDatabase::from_pool(db.pool().await, &Settings::default())
}
#[tokio::test]
async fn a_child_is_named_for_its_parent_and_the_step_that_started_it() {
let db = test_database().await;
let dbos = DBOS::new(config("child-id-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n * 2) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let first = child.run(21).await?;
let second = child.run(50).await?;
Ok::<u32, Error>(first + second)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "the-parent";
let total = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(total, 142, "both children ran");
let reader = reader(&db).await;
for (step_id, expected) in [(0, "the-parent-0"), (2, "the-parent-2")] {
let row = reader
.get_workflow(expected)
.await
.expect("read failed")
.unwrap_or_else(|| panic!("no row for {expected}"));
assert_eq!(row.status, WorkflowStatus::Success);
assert_eq!(
row.parent_workflow_id.as_deref(),
Some(id),
"the child's row points back at its parent"
);
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
let step = steps
.iter()
.find(|step| step.step_id == step_id)
.unwrap_or_else(|| panic!("no step {step_id} on the parent"));
assert_eq!(step.step_name, "child");
assert_eq!(
step.child_workflow_id.as_deref(),
Some(expected),
"the start is recorded against the parent"
);
}
let children = reader.get_workflow_children(id).await.expect("read failed");
assert_eq!(children.len(), 2, "two children: {children:?}");
dbos.shutdown().await;
}
#[tokio::test]
async fn a_start_dropped_after_it_begins_still_starts_the_child() {
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
let db = test_database().await;
let dbos = DBOS::new(config("dropped-start-app", &db));
let ran = Arc::new(AtomicU32::new(0));
let child = dbos
.register_workflow("child", {
let ran = Arc::clone(&ran);
move |n: u32| {
let ran = Arc::clone(&ran);
async move {
ran.fetch_add(1, Ordering::SeqCst);
Ok::<u32, Error>(n * 2)
}
}
})
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
let mut start = child.start(21);
assert_eq!(
start.step_id(),
Some(0),
"the start claimed its id at the call"
);
assert!(
matches!(
Pin::new(&mut start).poll(&mut Context::from_waker(Waker::noop())),
Poll::Pending
),
"one poll should reach the spawn and stop at the await behind it",
);
drop(start);
dbos::step("after", || async { Ok::<u32, Error>(1) }).await
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "the-dropping-parent";
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
let reader = reader(&db).await;
let child_id = format!("{id}-0");
let deadline = std::time::Instant::now() + Duration::from_secs(30);
loop {
if let Some(row) = reader.get_workflow(&child_id).await.expect("read failed")
&& row.status == WorkflowStatus::Success
{
assert_eq!(
row.parent_workflow_id.as_deref(),
Some(id),
"the child points back at the parent that dropped its start",
);
break;
}
assert!(
std::time::Instant::now() < deadline,
"the dropped start never produced a finished child",
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert_eq!(
ran.load(Ordering::SeqCst),
1,
"the child body ran exactly once"
);
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
let recorded: Vec<(i32, &str, Option<&str>)> = steps
.iter()
.map(|step| {
(
step.step_id,
step.step_name.as_str(),
step.child_workflow_id.as_deref(),
)
})
.collect();
assert_eq!(
recorded,
[(0, "child", Some(child_id.as_str())), (1, "after", None)],
"the dropped start recorded its own row and left `after` where it would have been anyway",
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_start_dropped_by_a_parent_that_then_finishes_still_starts_the_child() {
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
let db = test_database().await;
if db.backend() == Backend::Cockroach {
return;
}
let dbos = DBOS::new(config("outlived-start-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n * 2) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
let mut start = child.start(21);
assert!(matches!(
Pin::new(&mut start).poll(&mut Context::from_waker(Waker::noop())),
Poll::Pending
));
drop(start);
Ok::<u32, Error>(1)
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let pool = db.pool().await;
let mut lock = pool.begin().await.expect("begin failed");
sqlx::query("LOCK TABLE dbos.operation_outputs IN ACCESS EXCLUSIVE MODE")
.execute(&mut *lock)
.await
.expect("lock failed");
let id = "the-outlived-parent";
let handle = parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("the parent did not start");
let reader = reader(&db).await;
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while std::time::Instant::now() < deadline {
let row = reader.get_workflow(id).await.expect("read failed");
assert_ne!(
row.map(|row| row.status),
Some(WorkflowStatus::Success),
"the parent recorded its outcome while its start was still in flight",
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
lock.rollback().await.expect("unlock failed");
assert_eq!(handle.result().await.expect("the parent failed"), 1);
let child_id = format!("{id}-0");
let deadline = std::time::Instant::now() + Duration::from_secs(30);
loop {
if let Some(row) = reader.get_workflow(&child_id).await.expect("read failed")
&& row.status == WorkflowStatus::Success
{
break;
}
assert!(
std::time::Instant::now() < deadline,
"the start the parent outlived never produced a finished child",
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_parent_waiting_for_a_stalled_start_still_times_out() {
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
let db = test_database().await;
if db.backend() == Backend::Cockroach {
return;
}
let dbos = DBOS::new(config("stalled-start-timeout-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n * 2) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
let mut start = child.start(21);
assert!(matches!(
Pin::new(&mut start).poll(&mut Context::from_waker(Waker::noop())),
Poll::Pending
));
drop(start);
Ok::<u32, Error>(1)
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let pool = db.pool().await;
let mut lock = pool.begin().await.expect("begin failed");
sqlx::query("LOCK TABLE dbos.operation_outputs IN ACCESS EXCLUSIVE MODE")
.execute(&mut *lock)
.await
.expect("lock failed");
let handle = parent
.start_with(
(),
StartOptions {
workflow_id: Some("the-stalled-parent"),
timeout: Timeout::Explicit(Duration::from_secs(1)),
..StartOptions::default()
},
)
.await
.expect("the parent did not start");
let result = tokio::time::timeout(Duration::from_secs(20), handle.result())
.await
.expect("the parent was held past its deadline by the start it was waiting for");
assert!(
matches!(result, Err(Error::WorkflowCancelled { .. })),
"expected the parent to time out, got {result:?}"
);
lock.rollback().await.expect("unlock failed");
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_joining_a_held_key_is_recorded_as_the_workflow_it_joined() {
let db = test_database().await;
let dbos = DBOS::new(config("child-dedup-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(9) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
child
.start_with(
(),
StartOptions {
queue: Some(QueueRequest {
deduplication_id: Some("order-42"),
duplication_policy: DuplicationPolicy::ReturnExisting,
..QueueRequest::new("demo-queue")
}),
..StartOptions::default()
},
)
.await?
.result()
.await
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
dbos.register_queue(
"demo-queue",
QueueOptions::default(),
QueueConflict::UpdateIfLatestVersion,
)
.await
.expect("registration failed");
let holder = child
.start_with(
(),
StartOptions {
workflow_id: Some("the-holder"),
queue: Some(QueueRequest {
deduplication_id: Some("order-42"),
delay: Some(Duration::from_secs(3)),
..QueueRequest::new("demo-queue")
}),
..StartOptions::default()
},
)
.await
.expect("the holder was not enqueued");
let id = "the-joining-parent";
let result = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(result, 9, "the parent read the joined workflow's output");
let reader = reader(&db).await;
assert!(
reader
.get_workflow("the-joining-parent-0")
.await
.expect("read failed")
.is_none(),
"the derived child id names no row: the insert lost the key",
);
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
let start = steps
.iter()
.find(|step| step.step_id == 0)
.expect("no start step on the parent");
assert_eq!(start.step_name, "child");
assert_eq!(
start.child_workflow_id.as_deref(),
Some("the-holder"),
"the start records the workflow that was joined",
);
assert!(
reader
.get_workflow_children(id)
.await
.expect("read failed")
.is_empty(),
"a joined workflow is not the joining parent's child",
);
assert_eq!(
reader
.get_workflow("the-holder")
.await
.expect("read failed")
.expect("the holder is missing")
.parent_workflow_id,
None,
"the holder keeps its own parentage: joining does not re-parent it",
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(20), holder.result())
.await
.expect("the holder never ran")
.expect("the holder failed"),
9
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_deduplicated_child_start_replays_its_error_after_the_key_is_freed() {
let db = test_database().await;
let dbos = DBOS::new(config("child-dedup-error-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(9) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
let started = child
.start_with(
(),
StartOptions {
queue: Some(QueueRequest {
deduplication_id: Some("order-7"),
..QueueRequest::new("dedup-error-queue")
}),
..StartOptions::default()
},
)
.await;
match started {
Ok(handle) => Ok::<_, Error>(format!("started {}", handle.result().await?)),
Err(
e @ Error::SystemDatabase(dbos::sysdb::Error::QueueDeduplicated {
..
}),
) => Ok(format!("deduplicated: {e}")),
Err(e) => Err(e),
}
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let holder = child
.start_with(
(),
StartOptions {
workflow_id: Some("the-dedup-holder"),
queue: Some(QueueRequest {
deduplication_id: Some("order-7"),
..QueueRequest::new("dedup-error-queue")
}),
..StartOptions::default()
},
)
.await
.expect("the holder was not enqueued");
let id = "the-deduplicated-parent";
let first = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert!(first.starts_with("deduplicated: "), "{first}");
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
assert_eq!(steps.len(), 1, "{steps:?}");
assert_eq!(steps[0].step_name, "child");
assert!(
steps[0].error.is_some() && steps[0].child_workflow_id.is_none(),
"the error is recorded as the start's, with no child: {:?}",
steps[0]
);
dbos.register_queue(
"dedup-error-queue",
QueueOptions::default(),
QueueConflict::UpdateIfLatestVersion,
)
.await
.expect("registration failed");
assert_eq!(
tokio::time::timeout(Duration::from_secs(20), holder.result())
.await
.expect("the holder never ran")
.expect("the holder failed"),
9
);
let replayed = dbos
.fork::<String, Error>(id, 1)
.await
.expect("fork failed");
let replayed_id = replayed.workflow_id().to_owned();
assert_eq!(
replayed.result().await.expect("the replay failed"),
first,
"the replay took a different branch from the run it replayed"
);
assert!(
reader
.get_workflow(&format!("{replayed_id}-0"))
.await
.expect("read failed")
.is_none(),
"the replay started the child"
);
let rerun = dbos
.fork::<String, Error>(id, 0)
.await
.expect("fork failed");
assert_eq!(
tokio::time::timeout(Duration::from_secs(20), rerun.result())
.await
.expect("the rerun never finished")
.expect("the rerun failed"),
"started 9"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_start_refused_by_the_insert_replays_its_error_when_the_start_would_succeed() {
use std::sync::Mutex;
let db = test_database().await;
let dbos = DBOS::new(config("child-insert-error-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(9) })
.unwrap();
let key = Arc::new(Mutex::new(String::new()));
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
let key = Arc::clone(&key);
move |()| {
let child = child.clone();
let key = key.lock().unwrap().clone();
async move {
let started = child
.start_with(
(),
StartOptions {
queue: Some(QueueRequest::new(&key)),
..StartOptions::default()
},
)
.await;
match started {
Ok(handle) => Ok::<_, Error>(format!("started {}", handle.workflow_id())),
Err(e @ Error::InvalidArgument { .. }) => Ok(format!("refused: {e}")),
Err(e) => Err(e),
}
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "the-insert-refused-parent";
let first = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert!(
first.starts_with("refused: invalid argument to enqueue a workflow: queue ``: `name`"),
"{first}"
);
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
assert_eq!(steps.len(), 1, "{steps:?}");
assert_eq!(steps[0].step_name, "child");
assert!(
steps[0].error.is_some() && steps[0].child_workflow_id.is_none(),
"the refusal is recorded as the start's, with no child: {:?}",
steps[0]
);
*key.lock().unwrap() = "insert-error-queue".to_owned();
let replayed = dbos
.fork::<String, Error>(id, 1)
.await
.expect("fork failed");
let replayed_id = replayed.workflow_id().to_owned();
let replayed = replayed.result().await.expect("the replay failed");
let refusal = "`name` must be absent rather than empty";
assert!(
first.ends_with(refusal)
&& replayed.starts_with("refused: ")
&& replayed.ends_with(refusal),
"the replay took a different branch from the run it replayed: {replayed}"
);
assert!(
reader
.get_workflow(&format!("{replayed_id}-0"))
.await
.expect("read failed")
.is_none(),
"the replay started the child"
);
let rerun = dbos
.fork::<String, Error>(id, 0)
.await
.expect("fork failed");
let rerun_id = rerun.workflow_id().to_owned();
assert_eq!(
rerun.result().await.expect("the rerun failed"),
format!("started {rerun_id}-0")
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_id_taken_by_another_function_replays_its_error_after_the_row_is_deleted() {
let db = test_database().await;
let dbos = DBOS::new(config("child-conflict-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(9) })
.unwrap();
let squatter = dbos
.register_workflow("squatter", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
child
.start_with(
(),
StartOptions {
workflow_id: Some("the-taken-child-id"),
..StartOptions::default()
},
)
.await?
.result()
.await
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
squatter
.run_with(
(),
RunOptions {
workflow_id: Some("the-taken-child-id"),
..RunOptions::default()
},
)
.await
.expect("the squatter failed");
let id = "the-conflicting-parent";
let first = parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("the parent did not start")
.result()
.await
.expect_err("the parent should end with the error");
assert!(
matches!(
first,
Error::SystemDatabase(dbos::sysdb::Error::ConflictingWorkflow { .. })
),
"{first:?}"
);
let reader = reader(&db).await;
assert_eq!(
reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the parent is missing")
.status,
WorkflowStatus::Error,
"a recorded start error returned with `?` is the workflow's outcome"
);
dbos.delete("the-taken-child-id")
.await
.expect("delete failed");
let fork = dbos.fork::<u32, Error>(id, 1).await.expect("fork failed");
let fork_id = fork.workflow_id().to_owned();
let replayed = fork
.result()
.await
.expect_err("the replay should end with the error");
assert_eq!(
replayed.to_string(),
first.to_string(),
"the replay ended differently from the run it replayed"
);
assert_eq!(
reader
.get_workflow(&fork_id)
.await
.expect("read failed")
.expect("the fork is missing")
.status,
WorkflowStatus::Error,
"the replay's recorded start error is its outcome too"
);
assert!(
reader
.get_workflow("the-taken-child-id")
.await
.expect("read failed")
.is_none(),
"the replay started the child"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_rejected_child_start_replays_its_error_after_the_row_is_deleted() {
let db = test_database().await;
let dbos = DBOS::new(config("child-reuse-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(9) })
.unwrap();
let parent = dbos
.register_workflow("parent", {
let child = child.clone();
move |()| {
let child = child.clone();
async move {
child
.start_with(
(),
StartOptions {
workflow_id: Some("the-reused-child-id"),
workflow_id_reuse_policy: WorkflowIdReusePolicy::Reject,
..StartOptions::default()
},
)
.await?
.result()
.await
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
child
.run_with(
(),
RunOptions {
workflow_id: Some("the-reused-child-id"),
..RunOptions::default()
},
)
.await
.expect("the first child failed");
let id = "the-rejecting-parent";
let first = parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("the parent did not start")
.result()
.await
.expect_err("the parent should end with the error");
assert!(
matches!(
first,
Error::SystemDatabase(dbos::sysdb::Error::WorkflowIdInUse { .. })
),
"{first:?}"
);
dbos.delete("the-reused-child-id")
.await
.expect("delete failed");
let fork = dbos.fork::<u32, Error>(id, 1).await.expect("fork failed");
let replayed = fork
.result()
.await
.expect_err("the replay should end with the error");
assert_eq!(
replayed.to_string(),
first.to_string(),
"the replay ended differently from the run it replayed"
);
assert!(
reader(&db)
.await
.get_workflow("the-reused-child-id")
.await
.expect("read failed")
.is_none(),
"the replay started the child"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_recovered_parent_adopts_the_children_it_already_started() {
let db = test_database().await;
let id = "crashes-between-children";
let bodies = Arc::new(AtomicU32::new(0));
{
let dbos = DBOS::new(config("recover-children-app", &db));
let counted = Arc::clone(&bodies);
let child = dbos
.register_workflow("child", move |n: u32| {
let counted = Arc::clone(&counted);
async move {
counted.fetch_add(1, Ordering::SeqCst);
if n == 2 {
tokio::time::sleep(Duration::from_secs(30)).await;
}
Ok::<u32, Error>(n)
}
})
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let mut total = 0;
for n in 0..3 {
total += child.run(n).await?;
}
Ok::<u32, Error>(total)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("start failed");
let deadline = std::time::Instant::now() + Duration::from_secs(20);
while bodies.load(Ordering::SeqCst) < 3 {
assert!(
std::time::Instant::now() < deadline,
"the parent never got as far as its third child"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
dbos.shutdown().await;
}
let reader = reader(&db).await;
assert_eq!(
reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing")
.status,
WorkflowStatus::Pending,
"shutdown left the parent for recovery"
);
let ran_before = bodies.load(Ordering::SeqCst);
assert_eq!(
ran_before, 3,
"three bodies entered, the third still blocked when the process died"
);
let dbos = DBOS::new(config("recover-children-app", &db));
let recovered_bodies = Arc::clone(&bodies);
let child = dbos
.register_workflow("child", move |n: u32| {
let bodies = Arc::clone(&recovered_bodies);
async move {
bodies.fetch_add(1, Ordering::SeqCst);
Ok::<u32, Error>(n)
}
})
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let mut total = 0;
for n in 0..3 {
total += child.run(n).await?;
}
Ok::<u32, Error>(total)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
drop(parent);
let deadline = std::time::Instant::now() + Duration::from_secs(30);
loop {
let row = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing");
if row.status == WorkflowStatus::Success {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the recovered parent did not finish; status {:?}",
row.status
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
let children = reader.get_workflow_children(id).await.expect("read failed");
assert_eq!(
children.len(),
3,
"the recovered parent started no fourth child: {children:?}"
);
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
assert_eq!(
steps.len(),
6,
"a start and an await per child, and no second set: {steps:?}"
);
assert_eq!(
bodies.load(Ordering::SeqCst),
ran_before + 1,
"only the unfinished child ran again: the two that succeeded were adopted, not re-run"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn children_started_in_a_loop_run_concurrently() {
let db = test_database().await;
let dbos = DBOS::new(config("fan-out-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move {
tokio::time::sleep(Duration::from_millis(400)).await;
Ok::<u32, Error>(n)
})
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let mut handles = Vec::new();
for n in 0..3 {
handles.push(child.start(n).await?);
}
let mut total = 0;
for handle in handles {
total += handle.result().await?;
}
Ok::<u32, Error>(total)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let began = std::time::Instant::now();
let total = parent
.run_with(
(),
RunOptions {
workflow_id: Some("fans-out"),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
let took = began.elapsed();
assert_eq!(total, 3, "0 + 1 + 2");
assert!(
took < Duration::from_millis(1_200),
"three 400ms children took {took:?}, which is serial rather than concurrent"
);
let children = reader(&db)
.await
.get_workflow_children("fans-out")
.await
.expect("read failed");
assert_eq!(children.len(), 3);
dbos.shutdown().await;
}
#[tokio::test]
async fn child_starts_and_awaits_driven_out_of_build_order_keep_their_ids() {
let db = test_database().await;
let dbos = DBOS::new(config("out-of-order-children-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let a = child.start(1);
let b = child.start(2);
let c = child.start(3);
let (c, b, a) = tokio::join!(c, b, a);
let (a, b, c) = (a?, b?, c?);
let a = a.result();
let b = b.result();
let c = c.result();
let (c, b, a) = tokio::join!(c, b, a);
Ok::<u32, Error>(a? + b? + c?)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "drives-its-children-backwards";
let total = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(total, 6, "1 + 2 + 3");
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
let recorded: Vec<(i32, &str, Option<&str>)> = steps
.iter()
.map(|step| {
(
step.step_id,
step.step_name.as_str(),
step.child_workflow_id.as_deref(),
)
})
.collect();
assert_eq!(
recorded,
[
(0, "child", Some("drives-its-children-backwards-0")),
(1, "child", Some("drives-its-children-backwards-1")),
(2, "child", Some("drives-its-children-backwards-2")),
(3, "DBOS.getResult", Some("drives-its-children-backwards-0")),
(4, "DBOS.getResult", Some("drives-its-children-backwards-1")),
(5, "DBOS.getResult", Some("drives-its-children-backwards-2")),
],
"the three starts hold the first three ids and the three awaits the next three, both in \
the order they were written"
);
for (suffix, expected) in [("-0", "1"), ("-1", "2"), ("-2", "3")] {
let child_id = format!("{id}{suffix}");
let row = reader
.get_workflow(&child_id)
.await
.expect("read failed")
.unwrap_or_else(|| panic!("no row for {child_id}"));
assert_eq!(
row.output.as_deref(),
Some(expected),
"{child_id} ran the argument the start built at that position"
);
}
dbos.shutdown().await;
}
#[tokio::test]
async fn runs_driven_out_of_build_order_keep_their_pairs_of_step_ids() {
let db = test_database().await;
let dbos = DBOS::new(config("out-of-order-runs-app", &db));
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let a = child.run(1);
let b = child.run(2);
let c = child.run(3);
let (c, b, a) = tokio::join!(c, b, a);
Ok::<u32, Error>(a? + b? + c?)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "runs-its-children-backwards";
let total = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(total, 6, "1 + 2 + 3");
let steps = reader(&db)
.await
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
let recorded: Vec<(i32, &str, Option<&str>)> = steps
.iter()
.map(|step| {
(
step.step_id,
step.step_name.as_str(),
step.child_workflow_id.as_deref(),
)
})
.collect();
assert_eq!(
recorded,
[
(0, "child", Some("runs-its-children-backwards-0")),
(1, "DBOS.getResult", Some("runs-its-children-backwards-0")),
(2, "child", Some("runs-its-children-backwards-2")),
(3, "DBOS.getResult", Some("runs-its-children-backwards-2")),
(4, "child", Some("runs-its-children-backwards-4")),
(5, "DBOS.getResult", Some("runs-its-children-backwards-4")),
],
"each run's await sits immediately behind its own start"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_that_is_never_awaited_is_still_recorded() {
let db = test_database().await;
let dbos = DBOS::new(config("unawaited-child-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
child.start(()).await?;
Ok::<u32, Error>(0)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "forgets-its-child";
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
assert_eq!(steps.len(), 1, "the start is recorded: {steps:?}");
assert_eq!(
steps[0].child_workflow_id.as_deref(),
Some("forgets-its-child-0")
);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
let row = reader
.get_workflow("forgets-its-child-0")
.await
.expect("read failed")
.expect("the child has no row");
if row.status == WorkflowStatus::Success {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the abandoned child never finished; status {:?}",
row.status
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_cannot_be_started_from_inside_a_step() {
let db = test_database().await;
let dbos = DBOS::new(config("child-in-step-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
dbos::step::<u32, dbos::EngineOnly, _, _>("tries_to_spawn", || {
let child = child.clone();
async move { child.run(()).await }
})
.await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let error = parent
.run_with(
(),
RunOptions {
workflow_id: Some("spawns-in-a-step"),
..RunOptions::default()
},
)
.await
.expect_err("the parent succeeded");
assert!(
matches!(&error, Error::InsideStep { operation } if operation.contains("workflow")),
"expected an inside-a-step refusal, got {error:?}"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn an_assigned_child_id_wins_over_the_derived_one() {
let db = test_database().await;
let dbos = DBOS::new(config("child-assigned-id-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(7) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
child
.run_with(
(),
RunOptions {
workflow_id: Some("i-named-this-one"),
..RunOptions::default()
},
)
.await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "names-its-child";
assert_eq!(
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed"),
7
);
let reader = reader(&db).await;
let row = reader
.get_workflow("i-named-this-one")
.await
.expect("read failed")
.expect("the assigned id has no row");
assert_eq!(row.parent_workflow_id.as_deref(), Some(id));
assert!(
reader
.get_workflow("names-its-child-0")
.await
.expect("read failed")
.is_none(),
"the derived id was not used as well"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_workflow_started_outside_a_workflow_has_no_parent() {
let db = test_database().await;
let dbos = DBOS::new(config("root-app", &db));
let root = dbos
.register_workflow("root", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "a-root";
root.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the workflow failed");
let row = reader(&db)
.await
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing");
assert!(row.parent_workflow_id.is_none());
dbos.shutdown().await;
}
#[tokio::test]
async fn awaiting_a_child_is_recorded_as_a_step() {
let db = test_database().await;
let dbos = DBOS::new(config("await-checkpoint-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(99) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move { child.run(()).await }
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "records-its-await";
assert_eq!(
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed"),
99
);
let steps = reader(&db)
.await
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
assert_eq!(steps.len(), 2, "one start, one await: {steps:?}");
assert_eq!(steps[0].step_name, "child", "the start");
assert_eq!(steps[1].step_name, "DBOS.getResult", "the await");
assert_eq!(steps[1].output.as_deref(), Some("99"));
assert_eq!(
steps[1].child_workflow_id.as_deref(),
Some("records-its-await-0"),
"the await says which child it was waiting on"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_replayed_parent_reads_the_recorded_outcome_rather_than_waiting_again() {
let db = test_database().await;
let id = "already-knows";
let reader = reader(&db).await;
{
let dbos = DBOS::new(config("replay-await-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(5) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let value = child.run(()).await?;
tokio::time::sleep(Duration::from_secs(30)).await;
Ok::<u32, Error>(value)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("start failed");
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
if steps.len() == 2 {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the await was never recorded: {steps:?}"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
dbos.shutdown().await;
}
reader
.delete_workflows(&[&format!("{id}-0")], false, None)
.await
.expect("delete failed");
assert!(
reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.is_none(),
"the child was deleted"
);
let dbos = DBOS::new(config("replay-await-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(5) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move { child.run(()).await }
})
.unwrap();
dbos.launch().await.expect("launch failed");
drop(parent);
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
let row = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing");
if row.status == WorkflowStatus::Success {
assert_eq!(row.output.as_deref(), Some("5"), "from the recorded await");
break;
}
assert!(
std::time::Instant::now() < deadline,
"the replayed parent did not finish; status {:?}",
row.status
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_cancelled_child_is_an_awaited_cancellation_in_the_parent() {
let db = test_database().await;
let dbos = DBOS::new(config("cancelled-child-app", &db));
let child = dbos
.register_workflow("child", |()| async move {
tokio::time::sleep(Duration::from_secs(30)).await;
Ok::<u32, Error>(1)
})
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
child
.run_with(
(),
RunOptions {
timeout: Timeout::Explicit(Duration::from_millis(300)),
..RunOptions::default()
},
)
.await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "waits-on-a-doomed-child";
let error = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect_err("the parent succeeded");
match &error {
Error::AwaitedWorkflowCancelled { workflow_id } => {
assert_eq!(
workflow_id, "waits-on-a-doomed-child-0",
"the child, not the parent"
);
}
other => panic!("expected an awaited cancellation, got {other:?}"),
}
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
let await_step = steps
.iter()
.find(|step| step.step_name == "DBOS.getResult")
.expect("the await was not recorded");
let recorded = await_step.error.as_deref().expect("no error recorded");
assert!(
recorded.contains("AwaitedWorkflowCancelled"),
"the row says the awaited workflow was cancelled: {recorded}"
);
let row = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing");
assert_eq!(
row.status,
WorkflowStatus::Error,
"the parent failed; it was not itself cancelled"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_inherits_its_parents_deadline() {
let db = test_database().await;
let dbos = DBOS::new(config("inherit-deadline-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move { child.run(()).await }
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "hands-down-its-deadline";
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
timeout: Timeout::Explicit(Duration::from_secs(300)),
..Default::default()
},
)
.await
.expect("the parent failed");
let reader = reader(&db).await;
let parent_row = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing");
let child_row = reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.expect("the child has no row");
assert!(parent_row.deadline.is_some(), "the parent has a deadline");
assert_eq!(
child_row.deadline, parent_row.deadline,
"the same instant, not a fresh budget"
);
assert!(
child_row.timeout.is_none(),
"the child was given no timeout of its own; it has a deadline because its parent had one"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_childs_own_timeout_replaces_the_inherited_deadline() {
let db = test_database().await;
let dbos = DBOS::new(config("child-timeout-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
child
.run_with(
(),
RunOptions {
timeout: Timeout::Explicit(Duration::from_secs(3_600)),
..RunOptions::default()
},
)
.await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "child-asks-for-more";
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
timeout: Timeout::Explicit(Duration::from_secs(60)),
..Default::default()
},
)
.await
.expect("the parent failed");
let reader = reader(&db).await;
let parent_deadline = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing")
.deadline
.expect("the parent has a deadline");
let child_row = reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.expect("the child has no row");
assert!(
child_row.deadline.expect("the child has a deadline") > parent_deadline,
"the child's own timeout won: it outlives its parent"
);
assert_eq!(child_row.timeout, Some(Duration::from_secs(3_600)));
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_can_decline_the_inherited_deadline() {
let db = test_database().await;
let dbos = DBOS::new(config("child-no-timeout-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let inherited = child.run(()).await?;
let detached = child
.run_with(
(),
RunOptions {
timeout: Timeout::None,
..RunOptions::default()
},
)
.await?;
Ok::<u32, Error>(inherited + detached)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "one-child-opts-out";
let total = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
timeout: Timeout::Explicit(Duration::from_secs(300)),
..Default::default()
},
)
.await
.expect("the parent failed");
assert_eq!(total, 2, "both children ran");
let reader = reader(&db).await;
let parent_deadline = reader
.get_workflow(id)
.await
.expect("read failed")
.expect("the row is missing")
.deadline
.expect("the parent has a deadline");
let inherited = reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.expect("the first child has no row");
assert_eq!(
inherited.deadline,
Some(parent_deadline),
"silence inherits the parent's instant verbatim"
);
let detached = reader
.get_workflow(&format!("{id}-2"))
.await
.expect("read failed")
.expect("the second child has no row");
assert_eq!(
detached.deadline, None,
"Timeout::None declined the parent's deadline"
);
assert_eq!(
detached.timeout, None,
"and took no budget of its own in its place"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_parent_and_its_child_hit_an_inherited_deadline_independently() {
let db = test_database().await;
let dbos = DBOS::new(config("deadline-cascade-app", &db));
let child = dbos
.register_workflow("child", |()| async move {
tokio::time::sleep(Duration::from_secs(30)).await;
Ok::<u32, Error>(1)
})
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move { child.run(()).await }
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "runs-out-of-time";
let error = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
timeout: Timeout::Explicit(Duration::from_millis(400)),
..Default::default()
},
)
.await
.expect_err("the parent succeeded");
assert!(
matches!(error, Error::WorkflowCancelled { .. }),
"the parent was cancelled by its own deadline, got {error:?}"
);
let reader = reader(&db).await;
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
let row = reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.expect("the child has no row");
if row.status == WorkflowStatus::Cancelled {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the child was not cancelled by the deadline it inherited; status {:?}",
row.status
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
assert_eq!(
steps.len(),
1,
"the start, and nothing for the interrupted await: {steps:?}"
);
assert_eq!(steps[0].step_name, "child", "the start");
dbos.shutdown().await;
}
#[tokio::test]
async fn a_start_position_holding_a_plain_step_is_refused() {
let db = test_database().await;
let dbos = DBOS::new(config("stale-start-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let gate = Arc::new(std::sync::Mutex::new(Some(rx)));
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
let gate = Arc::clone(&gate);
async move {
let rx = gate.lock().unwrap().take().expect("the parent runs once");
let _ = rx.await;
child.run(()).await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "changed-under-itself";
let handle = parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("start failed");
let reader = reader(&db).await;
reader
.record_step(
id,
0,
"child",
Outcome::Output(Some("1")),
None,
None,
&owner_token(&reader, id).await,
)
.await
.expect("planting the step failed");
tx.send(()).expect("the parent is waiting on the gate");
let error = handle.result().await.expect_err("the parent succeeded");
match &error {
Error::SystemDatabase(dbos::sysdb::Error::UnexpectedStep {
step_id,
expected,
recorded,
..
}) => {
assert_eq!(*step_id, 0, "the start position");
assert!(
expected.contains("child workflow start"),
"says what it wanted: {expected}"
);
assert!(
recorded.contains("plain step"),
"and what it found: {recorded}"
);
}
other => panic!("expected an unexpected-step refusal, got {other:?}"),
}
assert!(
reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.is_none(),
"no child row was created before the refusal"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_start_refused_for_an_unstorable_duration_spends_no_step_id() {
let db = test_database().await;
let dbos = DBOS::new(config("refused-duration-child-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
for duration in [Duration::MAX, Duration::ZERO] {
let refused = child
.start_with(
(),
StartOptions {
timeout: Timeout::Explicit(duration),
..StartOptions::default()
},
)
.await;
assert!(
matches!(&refused, Err(Error::InvalidArgument { operation, detail })
if operation == "start a workflow" && detail.contains("`timeout`")),
"a timeout of {duration:?} should be refused, got {:?}",
refused.map(|_| ())
);
let refused = child
.start_with(
(),
StartOptions {
queue: Some(QueueRequest {
delay: Some(duration),
..QueueRequest::new("never-reached")
}),
..StartOptions::default()
},
)
.await;
assert!(
matches!(&refused, Err(Error::InvalidArgument { operation, detail })
if operation == "enqueue a workflow" && detail.contains("`delay`")),
"a delay of {duration:?} should be refused, got {:?}",
refused.map(|_| ())
);
}
dbos::step("after", || async { Ok(()) }).await?;
Ok::<u32, Error>(1)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "refuses-its-children";
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
let recorded: Vec<(i32, &str)> = steps
.iter()
.map(|s| (s.step_id, s.step_name.as_str()))
.collect();
assert_eq!(
recorded,
[(0, "after")],
"a refused start took the step id the step after it should have had",
);
for child_id in (0..4).map(|step| format!("{id}-{step}")) {
assert!(
reader
.get_workflow(&child_id)
.await
.expect("read failed")
.is_none(),
"a refused start created {child_id}"
);
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_started_through_another_instance_is_refused() {
let db = test_database().await;
let other = DBOS::new(config("other-instance-app", &db));
let owner = DBOS::new(config("owner-instance-app", &db));
let child = other
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let parent = {
let child = child.clone();
owner
.register_workflow("parent", move |()| {
let child = child.clone();
async move { child.run(()).await }
})
.unwrap()
};
other.launch().await.expect("launch failed");
owner.launch().await.expect("launch failed");
let id = "borrows-another-instance";
let error = parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect_err("the parent succeeded");
assert!(
matches!(&error, Error::WrongInstance { operation } if operation.contains("workflow")),
"expected a wrong-instance refusal, got {error:?}"
);
let reader = reader(&db).await;
assert!(
reader
.get_workflow(&format!("{id}-0"))
.await
.expect("read failed")
.is_none(),
"no child row"
);
assert!(
reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed")
.is_empty(),
"and no start recorded against the parent"
);
owner.shutdown().await;
other.shutdown().await;
}
#[tokio::test]
async fn awaiting_a_child_inside_a_step_is_covered_by_that_step() {
let db = test_database().await;
let dbos = DBOS::new(config("await-in-step-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(41) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let handle = child.start(()).await?;
let slot = Arc::new(std::sync::Mutex::new(Some(handle)));
let value = dbos::step("collect", move || {
let slot = Arc::clone(&slot);
async move {
let handle = slot.lock().unwrap().take().expect("the step runs once");
handle.result().await
}
})
.await?;
Ok::<u32, Error>(value)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "collects-inside-a-step";
assert_eq!(
parent
.run_with(
(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
.await
.expect("the parent failed"),
41
);
let steps = reader(&db)
.await
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed");
assert_eq!(steps.len(), 2, "the start and the step: {steps:?}");
assert_eq!(steps[0].step_name, "child", "the start");
assert_eq!(
steps[0].child_workflow_id.as_deref(),
Some("collects-inside-a-step-0")
);
assert_eq!(
steps[1].step_name, "collect",
"the enclosing step, under its own name"
);
assert_eq!(
steps[1].output.as_deref(),
Some("41"),
"which carries the child's value"
);
assert!(
steps.iter().all(|step| step.step_name != "DBOS.getResult"),
"the await added no checkpoint of its own: {steps:?}"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_recorded_await_of_another_workflow_is_refused() {
let db = test_database().await;
let dbos = DBOS::new(config("stale-await-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(1) })
.unwrap();
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let gate = Arc::new(std::sync::Mutex::new(Some(rx)));
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
let gate = Arc::clone(&gate);
async move {
let handle = child.start(()).await?;
let rx = gate.lock().unwrap().take().expect("the parent runs once");
let _ = rx.await;
handle.result().await
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let id = "awaits-the-wrong-one";
let handle = parent
.start_with(
(),
StartOptions {
workflow_id: Some(id),
..StartOptions::default()
},
)
.await
.expect("start failed");
let reader = reader(&db).await;
let deadline = std::time::Instant::now() + Duration::from_secs(20);
loop {
let steps = reader
.list_workflow_steps(id, false, None, None, None)
.await
.expect("read failed");
if !steps.is_empty() {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the parent never recorded its start"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
reader
.record_child_result(
id,
1,
"somebody-elses-workflow",
Outcome::Output(Some("7")),
None,
None,
&owner_token(&reader, id).await,
)
.await
.expect("planting the await failed");
tx.send(()).expect("the parent is waiting on the gate");
let error = handle.result().await.expect_err("the parent succeeded");
match &error {
Error::SystemDatabase(dbos::sysdb::Error::UnexpectedStep {
step_id,
expected,
recorded,
..
}) => {
assert_eq!(*step_id, 1, "the await position");
assert!(
expected.contains("awaits-the-wrong-one-0"),
"says which workflow it was awaiting: {expected}"
);
assert!(
recorded.contains("somebody-elses-workflow"),
"and whose outcome it found: {recorded}"
);
}
other => panic!("expected an unexpected-step refusal, got {other:?}"),
}
dbos.shutdown().await;
}
#[tokio::test]
async fn a_child_that_fails_differently_is_started_through_lift() {
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("the shipment was refused")]
struct Refused;
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("billing gave up")]
struct GaveUp;
let db = test_database().await;
let dbos = DBOS::new(config("mixed-channels-app", &db));
let ship = dbos
.register_workflow("ship", |()| async {
Err::<u32, Error<Refused>>(Error::Application(Refused))
})
.unwrap();
let bill = dbos
.register_workflow("bill", {
let ship = ship.clone();
move |()| {
let ship = ship.clone();
async move {
let handle = ship.start(()).lift().await?;
let refused = matches!(handle.result().await, Err(Error::Application(Refused)));
if !refused {
Err(GaveUp)?;
}
let refused_start = dbos::step::<(), GaveUp, _, _>("tries_to_start", || {
let ship = ship.clone();
async move { ship.start(()).lift::<GaveUp>().await.map(|_| ()) }
})
.await;
assert!(
matches!(refused_start, Err(Error::InsideStep { .. })),
"expected an inside-a-step refusal in the parent's channel, \
got {refused_start:?}"
);
Ok::<_, Error<GaveUp>>(refused)
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
assert!(
bill.run(()).await.expect("the parent failed"),
"the parent started a child of another error type and read its refusal back"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_select_step_races_a_step_against_a_childs_result() {
let db = test_database().await;
let dbos = DBOS::new(config("race-child-app", &db));
let child = dbos
.register_workflow("child", |()| async move { Ok::<u32, Error>(7) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |()| {
let child = child.clone();
async move {
let started = child.start(()).await?;
let outcome: u32 = dbos::select_step! {
slow = dbos::step("slow", || async {
std::future::pending::<()>().await;
Ok::<u32, Error>(0)
}) => slow?,
finished = started.result() => finished?
}?;
Ok::<u32, Error>(outcome)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let outcome = parent
.run_with(
(),
RunOptions {
workflow_id: Some("raced"),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(
outcome, 7,
"the await won and its arm produced the child's answer"
);
let steps = reader(&db)
.await
.list_workflow_steps("raced", false, None, None, None)
.await
.expect("read failed");
let positions: Vec<_> = steps
.iter()
.map(|step| {
(
step.step_id,
step.step_name.as_str(),
step.child_workflow_id.as_deref(),
)
})
.collect();
assert_eq!(
positions,
[
(0, "child", Some("raced-0")),
(2, "DBOS.getResult", Some("raced-0")),
(3, "DBOS.selectStep", None),
],
"the await keeps its build-order id and the race records the winner behind it"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_control_signal_winning_a_select_step_records_no_winner() {
let db = test_database().await;
let dbos = DBOS::new(config("control-race-app", &db));
let raced = Arc::new(tokio::sync::Notify::new());
let parent = {
let raced = Arc::clone(&raced);
dbos.register_workflow("parent", move |()| {
let raced = Arc::clone(&raced);
async move {
let outcome: Result<Result<u32, Error>, Error> = dbos::select_step! {
interrupted = dbos::step("interrupted", || async {
Err::<u32, Error>(Error::Interrupted {
workflow_id: "raced".to_owned(),
})
}) => interrupted,
slow = dbos::step("slow", || async {
std::future::pending::<()>().await;
Ok::<u32, Error>(1)
}) => slow,
};
raced.notify_one();
outcome.and_then(|won| won)
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let handle = parent
.start_with(
(),
StartOptions {
workflow_id: Some("raced"),
..StartOptions::default()
},
)
.await
.expect("the parent failed to start");
tokio::time::timeout(Duration::from_secs(5), raced.notified())
.await
.expect("the race did not finish");
let steps: Vec<_> = reader(&db)
.await
.list_workflow_steps("raced", false, None, None, None)
.await
.expect("read failed")
.iter()
.map(|step| (step.step_id, step.step_name.clone()))
.collect();
assert_eq!(
steps,
[],
"neither the interrupted step nor the race it won left a row"
);
drop(handle);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_losing_step_has_its_cancellation_token_fired() {
let db = test_database().await;
let dbos = DBOS::new(config("loser-cancelled-app", &db));
let released = Arc::new(tokio::sync::Notify::new());
let workflow = {
let released = Arc::clone(&released);
dbos.register_workflow("workflow", move |()| {
let released = Arc::clone(&released);
async move {
let (watching, watching_rx) = tokio::sync::oneshot::channel::<()>();
let mut watching = Some(watching);
let mut watching_rx_slot = Some(watching_rx);
let outcome: u32 = dbos::select_step! {
fast = dbos::step("fast", move || {
let watching_rx = watching_rx_slot.take();
async move {
if let Some(watching_rx) = watching_rx {
let _ = watching_rx.await;
}
Ok::<u32, Error>(1)
}
}) => fast?,
slow = dbos::step("slow", move || {
let released = Arc::clone(&released);
let watching = watching.take();
async move {
let token = dbos::cancellation_token();
tokio::spawn(async move {
token.cancelled().await;
released.notify_one();
});
if let Some(watching) = watching {
let _ = watching.send(());
}
std::future::pending::<()>().await;
Ok::<u32, Error>(2)
}
}) => slow?,
}?;
Ok::<u32, Error>(outcome)
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let outcome = workflow.run(()).await.expect("the workflow failed");
assert_eq!(outcome, 1, "the fast step won");
tokio::time::timeout(Duration::from_secs(5), released.notified())
.await
.expect("the losing step's token never fired");
dbos.shutdown().await;
}
#[tokio::test]
async fn a_workflow_enqueued_by_name_is_a_child_and_a_replay_does_not_enqueue_it_again() {
let db = test_database().await;
let dbos = DBOS::new(config("by-name-app", &db));
dbos.register_workflow("worker", |n: u32| async move { Ok::<u32, Error>(n * 2) })
.unwrap();
let parent = {
let instance = dbos.clone();
dbos.register_workflow("parent", move |n: u32| {
let instance = instance.clone();
async move {
let handle: dbos::WorkflowHandle<u32> =
instance.enqueue("worker", "by-name-queue", n).await?;
handle.result().await
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
dbos.register_queue(
"by-name-queue",
QueueOptions::default(),
QueueConflict::UpdateIfLatestVersion,
)
.await
.expect("registration failed");
let parent_id = "by-name-parent";
let doubled = parent
.run_with(
21,
RunOptions {
workflow_id: Some(parent_id),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert_eq!(doubled, 42);
let reader = reader(&db).await;
let child = reader
.get_workflow("by-name-parent-0")
.await
.unwrap()
.expect("the child is named for its parent and step");
assert_eq!(child.name.as_deref(), Some("worker"));
assert_eq!(child.parent_workflow_id.as_deref(), Some(parent_id));
assert_eq!(child.queue_name.as_deref(), Some("by-name-queue"));
let steps = reader
.list_workflow_steps(parent_id, true, None, None, None)
.await
.unwrap();
assert_eq!(steps[0].step_name, "worker");
assert_eq!(
steps[0].child_workflow_id.as_deref(),
Some("by-name-parent-0")
);
let replayed = dbos
.fork::<u32, dbos::EngineOnly>(parent_id, 1)
.await
.expect("fork failed")
.result()
.await
.expect("the replay failed");
assert_eq!(replayed, 42);
let workers = reader
.list_workflows(
&dbos::sysdb::types::WorkflowFilter {
names: vec!["worker"],
..Default::default()
},
None,
)
.await
.unwrap();
assert_eq!(workers.len(), 1, "the replay enqueued again: {workers:?}");
dbos.shutdown().await;
}
#[tokio::test]
async fn an_enqueue_by_name_writes_a_clients_row_and_is_refused_inside_a_step() {
let db = test_database().await;
let dbos = DBOS::new(config("by-name-row-app", &db));
let in_step = {
let instance = dbos.clone();
dbos.register_workflow("in_step", move |()| {
let instance = instance.clone();
async move {
dbos::step("enqueues", || {
let instance = instance.clone();
async move {
let _: dbos::WorkflowHandle<()> =
instance.enqueue("remote", "idle-queue", ()).await?;
Ok::<_, Error>(())
}
})
.await
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let _: dbos::WorkflowHandle<u32> = dbos
.enqueue_with(
"remote",
1u32,
dbos::EnqueueOptions {
workflow_id: Some("own-app"),
..dbos::EnqueueOptions::new("idle-queue")
},
)
.await
.expect("enqueue failed");
let _: dbos::WorkflowHandle<u32> = dbos
.enqueue_with(
"remote",
2u32,
dbos::EnqueueOptions {
workflow_id: Some("other-app"),
app_name: Some("by-name-other-app"),
app_version: Some("v9"),
..dbos::EnqueueOptions::new("idle-queue")
},
)
.await
.expect("enqueue failed");
let reader = reader(&db).await;
let own = reader.get_workflow("own-app").await.unwrap().unwrap();
assert_eq!(own.status, WorkflowStatus::Enqueued);
assert_eq!(own.name.as_deref(), Some("remote"));
assert_eq!(own.app_name.as_deref(), Some("by-name-row-app"));
assert_eq!(own.app_version, None, "no version unless one is named");
assert_eq!(own.parent_workflow_id, None);
let other = reader.get_workflow("other-app").await.unwrap().unwrap();
assert_eq!(other.app_name.as_deref(), Some("by-name-other-app"));
assert_eq!(other.app_version.as_deref(), Some("v9"));
let refused = in_step.run(()).await.unwrap_err();
assert!(matches!(refused, Error::InsideStep { .. }), "{refused:?}");
let empty: dbos::Result<dbos::WorkflowHandle<()>> = dbos.enqueue("", "idle-queue", ()).await;
assert!(
matches!(empty, Err(Error::InvalidArgument { .. })),
"{empty:?}"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_deduplicated_enqueue_by_name_replays_the_way_it_went() {
let db = test_database().await;
let dbos = DBOS::new(config("by-name-dedup-app", &db));
let parent = {
let instance = dbos.clone();
dbos.register_workflow("dedup_parent", move |policy: String| {
let instance = instance.clone();
async move {
let duplication_policy = match policy.as_str() {
"join" => DuplicationPolicy::ReturnExisting,
_ => DuplicationPolicy::Reject,
};
let enqueued: dbos::Result<dbos::WorkflowHandle<()>> = instance
.enqueue_with(
"remote",
(),
dbos::EnqueueOptions {
deduplication_id: Some("held-key"),
duplication_policy,
..dbos::EnqueueOptions::new("idle-dedup-queue")
},
)
.await;
match enqueued {
Ok(handle) => Ok::<_, Error>(format!("joined {}", handle.workflow_id())),
Err(Error::SystemDatabase(dbos::sysdb::Error::QueueDeduplicated {
..
})) => Ok("deduplicated".to_owned()),
Err(e) => Err(e),
}
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let _: dbos::WorkflowHandle<()> = dbos
.enqueue_with(
"remote",
(),
dbos::EnqueueOptions {
workflow_id: Some("key-holder"),
deduplication_id: Some("held-key"),
..dbos::EnqueueOptions::new("idle-dedup-queue")
},
)
.await
.expect("enqueue failed");
let run = |id: &'static str, policy: &str| {
parent.run_with(
policy.to_owned(),
RunOptions {
workflow_id: Some(id),
..RunOptions::default()
},
)
};
assert_eq!(run("joins", "join").await.unwrap(), "joined key-holder");
assert_eq!(run("refused", "reject").await.unwrap(), "deduplicated");
let reader = reader(&db).await;
let steps = reader
.list_workflow_steps("joins", true, None, None, None)
.await
.unwrap();
assert_eq!(steps[0].child_workflow_id.as_deref(), Some("key-holder"));
let replay = |id: &'static str| {
let dbos = dbos.clone();
async move {
dbos.fork::<String, Error>(id, 1)
.await
.expect("fork failed")
.result()
.await
.expect("the replay failed")
}
};
assert_eq!(replay("joins").await, "joined key-holder");
dbos.delete("key-holder").await.expect("delete failed");
assert_eq!(replay("refused").await, "deduplicated");
let remote = reader
.list_workflows(
&dbos::sysdb::types::WorkflowFilter {
names: vec!["remote"],
..Default::default()
},
None,
)
.await
.unwrap();
assert!(remote.is_empty(), "a replay enqueued again: {remote:?}");
dbos.shutdown().await;
}
#[tokio::test]
async fn an_enqueue_by_name_inherits_its_parents_deadline() {
let db = test_database().await;
let dbos = DBOS::new(config("by-name-deadline-app", &db));
let parent = {
let instance = dbos.clone();
dbos.register_workflow("deadline_parent", move |()| {
let instance = instance.clone();
async move {
let _: dbos::WorkflowHandle<()> = instance
.enqueue("remote", "idle-deadline-queue", ())
.await?;
let _: dbos::WorkflowHandle<()> = instance
.enqueue_with(
"remote",
(),
dbos::EnqueueOptions {
timeout: Timeout::Explicit(Duration::from_secs(3600)),
..dbos::EnqueueOptions::new("idle-deadline-queue")
},
)
.await?;
let _: dbos::WorkflowHandle<()> = instance
.enqueue_with(
"remote",
(),
dbos::EnqueueOptions {
timeout: Timeout::None,
..dbos::EnqueueOptions::new("idle-deadline-queue")
},
)
.await?;
Ok::<_, Error>(())
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
parent
.start_with(
(),
StartOptions {
workflow_id: Some("deadline-parent"),
timeout: Timeout::Explicit(Duration::from_secs(600)),
..StartOptions::default()
},
)
.await
.expect("start failed")
.result()
.await
.expect("the parent failed");
let reader = reader(&db).await;
let parent_row = reader
.get_workflow("deadline-parent")
.await
.unwrap()
.unwrap();
let inherited = reader
.get_workflow("deadline-parent-0")
.await
.unwrap()
.unwrap();
let own = reader
.get_workflow("deadline-parent-1")
.await
.unwrap()
.unwrap();
assert!(parent_row.deadline.is_some());
assert_eq!(inherited.deadline, parent_row.deadline);
assert_eq!(own.timeout, Some(Duration::from_secs(3600)));
assert_ne!(own.deadline, parent_row.deadline);
let unbounded = reader
.get_workflow("deadline-parent-2")
.await
.unwrap()
.unwrap();
assert_eq!((unbounded.timeout, unbounded.deadline), (None, None));
dbos.shutdown().await;
}
#[tokio::test]
async fn bad_enqueue_options_are_refused_before_the_step_id_is_taken() {
let db = test_database().await;
let dbos = DBOS::new(config("by-name-refused-app", &db));
let parent = {
let instance = dbos.clone();
dbos.register_workflow("refusing_parent", move |()| {
let instance = instance.clone();
async move {
let mut refusals = Vec::new();
for options in [
dbos::EnqueueOptions {
app_name: Some("Not-An-App"),
..dbos::EnqueueOptions::new("idle-queue")
},
dbos::EnqueueOptions {
app_version: Some(""),
..dbos::EnqueueOptions::new("idle-queue")
},
dbos::EnqueueOptions {
workflow_id: Some(""),
..dbos::EnqueueOptions::new("idle-queue")
},
dbos::EnqueueOptions {
deduplication_id: Some(""),
..dbos::EnqueueOptions::new("idle-queue")
},
dbos::EnqueueOptions {
partition_key: Some(""),
..dbos::EnqueueOptions::new("idle-queue")
},
] {
let refused: dbos::Result<dbos::WorkflowHandle<()>> =
instance.enqueue_with("remote", (), options).await;
match refused {
Err(Error::InvalidArgument { detail, .. }) => refusals.push(detail),
other => panic!("not refused: {:?}", other.map(|_| ())),
}
}
dbos::step("after", || async { Ok::<_, Error>(()) }).await?;
Ok::<_, Error>(refusals)
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let refusals = parent
.run_with(
(),
RunOptions {
workflow_id: Some("refusing-parent"),
..RunOptions::default()
},
)
.await
.expect("the parent failed");
assert!(refusals[0].contains("`app_name`"), "{refusals:?}");
assert!(refusals[1].contains("`app_version`"), "{refusals:?}");
assert!(refusals[2].contains("`workflow_id`"), "{refusals:?}");
assert!(refusals[3].contains("`deduplication_id`"), "{refusals:?}");
assert!(refusals[4].contains("`partition_key`"), "{refusals:?}");
let steps = reader(&db)
.await
.list_workflow_steps("refusing-parent", true, None, None, None)
.await
.unwrap();
assert_eq!(steps.len(), 1, "{steps:?}");
assert_eq!(
(steps[0].step_id, steps[0].step_name.as_str()),
(0, "after")
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_captured_workflow_ref_does_not_keep_the_instance_alive() {
let db = test_database().await;
let dbos = DBOS::new(config("captured-ref-app", &db));
let probe = Arc::new(());
let freed = Arc::downgrade(&probe);
let child = dbos
.register_workflow("child", |n: u32| async move { Ok::<u32, Error>(n * 2) })
.unwrap();
let parent = dbos
.register_workflow("parent", move |n: u32| {
let child = child.clone();
let _probe = Arc::clone(&probe);
async move {
let started = child.start(n).await?.result().await?;
let ran = child.run(n + 1).await?;
let debounced = Debouncer::new(&child)
.debounce("captured", Duration::from_millis(10), n + 2)
.await?
.result()
.await?;
Ok::<u32, Error>(started + ran + debounced)
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
assert_eq!(parent.run(1).await.expect("the parent failed"), 2 + 4 + 6);
dbos.shutdown().await;
drop(dbos);
assert!(
freed.upgrade().is_none(),
"the instance is still alive: something holds it after its last handle was dropped"
);
let error = parent
.start(1)
.await
.expect_err("started on a dropped instance");
assert!(
matches!(&error, Error::NotLaunched { .. }),
"expected not-launched, got {error:?}"
);
}