#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
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::WorkflowStatus;
use dbos::{Config, DBOS, Error};
use dbos_test_support::{TestDatabase, test_database};
const DEADLINE: Duration = Duration::from_secs(60);
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())
}
#[dbos_test_support::test]
async fn progress_events_survive_recovery_without_republishing() {
let one = Arc::new(AtomicU32::new(0));
let reached_gate = Arc::new(tokio::sync::Notify::new());
let release_gate = Arc::new(tokio::sync::Notify::new());
let db = test_database().await;
let dbos = DBOS::new(config("events-app", &db));
let publishes = {
let one = Arc::clone(&one);
let (reached, release) = (Arc::clone(&reached_gate), Arc::clone(&release_gate));
dbos.register_workflow("publishes", move |()| {
let one = Arc::clone(&one);
let (reached, release) = (Arc::clone(&reached), Arc::clone(&release));
async move {
dbos::step("one", || {
let one = Arc::clone(&one);
async move {
one.fetch_add(1, Ordering::SeqCst);
Ok(())
}
})
.await?;
dbos::set_event("progress", &1u32).await?;
reached.notify_one();
release.notified().await;
dbos::set_event("progress", &2u32).await?;
Ok::<_, dbos::Error>(())
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
let running = dbos::__rt::spawn(async move { publishes.run(()).await });
dbos::__rt::timeout(DEADLINE, reached_gate.notified())
.await
.expect("the workflow never reached its gate");
let reader = reader(&db).await;
let rows = reader
.list_workflows(&Default::default(), None)
.await
.expect("read failed");
let workflow_id = rows[0].workflow_id.clone();
let progress: Option<u32> = dbos
.get_event(&workflow_id, "progress", Duration::ZERO)
.await
.expect("get_event failed");
assert_eq!(progress, Some(1), "the value published so far");
let missing: Option<u32> = dbos
.get_event(&workflow_id, "no_such_key", Duration::ZERO)
.await
.expect("get_event failed");
assert_eq!(missing, None, "absence is a value, not an error");
dbos.shutdown().await;
let err = running.await.expect("the task panicked").unwrap_err();
assert!(matches!(err, Error::Interrupted { .. }), "{err}");
dbos.launch().await.expect("relaunch failed");
dbos::__rt::timeout(DEADLINE, reached_gate.notified())
.await
.expect("the recovered workflow never reached its gate again");
let progress: Option<u32> = dbos
.get_event(&workflow_id, "progress", Duration::ZERO)
.await
.expect("get_event failed");
assert_eq!(
progress,
Some(1),
"replay did not republish or lose the value"
);
release_gate.notify_one();
dbos::__rt::timeout(DEADLINE, async {
loop {
let row = reader
.get_workflow(&workflow_id)
.await
.expect("read failed")
.expect("the row exists");
if row.status == WorkflowStatus::Success {
return;
}
dbos::__rt::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("the workflow never finished");
let progress: Option<u32> = dbos
.get_event(&workflow_id, "progress", Duration::ZERO)
.await
.expect("get_event failed");
assert_eq!(progress, Some(2));
assert_eq!(one.load(Ordering::SeqCst), 1, "the step ran exactly once");
let steps = reader
.list_workflow_steps(&workflow_id, true, None, None, None)
.await
.expect("read failed");
let seen: Vec<(i32, &str)> = steps
.iter()
.map(|s| (s.step_id, s.step_name.as_str()))
.collect();
assert_eq!(
seen,
[(0, "one"), (1, "DBOS.setEvent"), (2, "DBOS.setEvent")]
);
dbos.shutdown().await;
}
#[dbos_test_support::test]
async fn a_reading_workflow_is_checkpointed_and_a_reading_step_is_not() {
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("the reader failed")]
struct ReaderError;
let db = test_database().await;
let dbos = DBOS::new(config("read-app", &db));
let publisher = dbos
.register_workflow("publisher", |()| async {
dbos::set_event("answer", &42u32).await?;
Ok::<_, dbos::Error>(())
})
.unwrap();
let in_workflow = dbos
.register_workflow("in_workflow", |publisher_id: String| async move {
let answer: Option<u32> =
dbos::get_event(&publisher_id, "answer", Duration::ZERO).await?;
Ok::<_, Error<ReaderError>>(answer)
})
.unwrap();
let in_step = dbos
.register_workflow("in_step", |publisher_id: String| async move {
let answer = dbos::step("read", || {
let publisher_id = publisher_id.clone();
async move {
let answer: Option<u32> =
dbos::get_event(&publisher_id, "answer", Duration::ZERO).await?;
Ok(answer)
}
})
.await?;
Ok::<_, dbos::Error>(answer)
})
.unwrap();
let via_instance = {
let dbos = dbos.clone();
dbos.clone()
.register_workflow("via_instance", move |publisher_id: String| {
let dbos = dbos.clone();
async move {
let answer: Option<u32> = dbos
.get_event(&publisher_id, "answer", Duration::ZERO)
.await
.map_err(Error::lift)?;
Ok::<_, Error<ReaderError>>(answer)
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
publisher.run(()).await.expect("the publisher failed");
let reader = reader(&db).await;
let publisher_id = reader
.list_workflows(&Default::default(), None)
.await
.expect("read failed")
.iter()
.find(|r| r.name.as_deref() == Some("publisher"))
.expect("the publisher ran")
.workflow_id
.clone();
assert_eq!(
in_workflow
.run(publisher_id.clone())
.await
.expect("the reader failed"),
Some(42)
);
assert_eq!(
in_step
.run(publisher_id.clone())
.await
.expect("the reader failed"),
Some(42)
);
assert_eq!(
via_instance
.run(publisher_id.clone())
.await
.expect("the reader failed"),
Some(42)
);
let steps_of = |name: &'static str| {
let reader = &reader;
async move {
let rows = reader
.list_workflows(&Default::default(), None)
.await
.expect("read failed");
let id = &rows
.iter()
.find(|r| r.name.as_deref() == Some(name))
.unwrap_or_else(|| panic!("no row for {name}"))
.workflow_id;
reader
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed")
.iter()
.map(|s| (s.step_id, s.step_name.clone()))
.collect::<Vec<_>>()
}
};
assert_eq!(
steps_of("in_workflow").await,
[
(0, "DBOS.getEvent".to_owned()),
(1, "DBOS.sleep".to_owned())
],
"the read and its deadline are checkpointed"
);
assert_eq!(
steps_of("in_step").await,
[(0, "read".to_owned())],
"the step is the only checkpoint; the read inside it left no ids"
);
assert_eq!(
steps_of("via_instance").await,
[
(0, "DBOS.getEvent".to_owned()),
(1, "DBOS.sleep".to_owned())
],
"the instance method checkpoints a workflow's read the same way"
);
dbos.shutdown().await;
}
#[dbos_test_support::test]
async fn reading_through_another_instance_from_inside_a_workflow_is_refused() {
let db = test_database().await;
let other = DBOS::new(config("other-app", &db));
let owner = DBOS::new(config("owner-app", &db));
let reads_through_other = {
let other = other.clone();
owner
.register_workflow("reads_through_other", move |()| {
let other = other.clone();
async move {
let answer: Option<u32> =
other.get_event("wf-1", "answer", Duration::ZERO).await?;
Ok::<_, dbos::Error>(answer)
}
})
.unwrap()
};
let in_step = {
let other = other.clone();
owner
.register_workflow("reads_in_step", move |()| {
let other = other.clone();
async move {
let answer = dbos::step("read", || {
let other = other.clone();
async move {
let answer: Option<u32> =
other.get_event("wf-1", "answer", Duration::ZERO).await?;
Ok(answer)
}
})
.await?;
Ok::<_, dbos::Error>(answer)
}
})
.unwrap()
};
other.launch().await.expect("launch failed");
owner.launch().await.expect("launch failed");
let err = reads_through_other.run(()).await.unwrap_err();
assert!(matches!(err, Error::WrongInstance { .. }), "{err}");
assert_eq!(
in_step.run(()).await.expect("a plain read is allowed"),
None,
"no such workflow, so no such event — but not a refusal"
);
owner.shutdown().await;
other.shutdown().await;
}
#[dbos_test_support::test]
async fn set_event_refuses_to_run_outside_a_workflow_or_inside_a_step() {
let outside: dbos::Result<()> = dbos::set_event("key", &1u32).await;
assert!(
matches!(outside.unwrap_err(), Error::NotInWorkflow { .. }),
"no workflow, no step sequence to checkpoint against"
);
let outside: dbos::Result<Option<u32>> = dbos::get_event("wf-1", "key", Duration::ZERO).await;
assert!(
matches!(outside.unwrap_err(), Error::NotInWorkflow { .. }),
"no workflow, no executor to read through"
);
let db = test_database().await;
let dbos = DBOS::new(config("guard-app", &db));
let in_step = dbos
.register_workflow("in_step", |()| async {
dbos::step("publishes", || async {
dbos::set_event("key", &1u32).await
})
.await?;
Ok::<_, dbos::Error>(())
})
.unwrap();
dbos.launch().await.expect("launch failed");
let err = in_step.run(()).await.unwrap_err();
assert!(matches!(err, Error::InsideStep { .. }), "{err}");
dbos.shutdown().await;
}
#[dbos_test_support::test]
async fn library_calls_driven_out_of_build_order_keep_the_ids_they_were_built_with() {
let db = test_database().await;
let dbos = DBOS::new(config("join-app", &db));
let joins = dbos
.register_workflow("joins", |()| async {
let a = dbos::sleep(Duration::from_millis(1));
let b = dbos::set_event("b", &1u32);
let c = dbos::get_event::<u32, _>("no-such-workflow", "nothing", Duration::ZERO);
let d = dbos::step("after", || async { Ok(()) });
assert_eq!(
(a.step_id(), b.step_id(), c.step_id(), d.step_id()),
(Some(0), Some(1), Some(2), Some(4)),
"the ids were taken at the call, in source order, before anything was polled"
);
let (d, c, b, a) = tokio::join!(d, c, b, a);
assert_eq!(c?, None, "no such workflow, so no such event");
b?;
a?;
d?;
Ok::<_, dbos::Error>(())
})
.unwrap();
dbos.launch().await.expect("launch failed");
let workflow_id = "joins-out-of-order";
joins
.run_with(
(),
dbos::RunOptions {
workflow_id: Some(workflow_id),
..Default::default()
},
)
.await
.expect("the workflow failed");
let steps = reader(&db)
.await
.list_workflow_steps(workflow_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, "DBOS.sleep"),
(1, "DBOS.setEvent"),
(2, "DBOS.getEvent"),
(3, "DBOS.sleep"),
(4, "after"),
],
"the ids follow the order the calls were built in, not the order they were polled in",
);
dbos.shutdown().await;
}
#[dbos_test_support::test]
async fn a_refused_call_spends_no_step_id() {
let db = test_database().await;
let dbos = DBOS::new(config("refusal-app", &db));
let refused = dbos
.register_workflow("refused", |()| async {
let unencodable: std::collections::BTreeMap<(u8, u8), u8> =
[((1, 2), 3)].into_iter().collect();
let refused: dbos::Result<()> = dbos::set_event("nope", &unencodable).await;
assert!(
matches!(refused.unwrap_err(), Error::Serialization { .. }),
"an unencodable event value should be refused"
);
dbos::step("after", || async { Ok(()) }).await?;
Ok::<_, dbos::Error>(())
})
.unwrap();
dbos.launch().await.expect("launch failed");
let workflow_id = "spends-nothing";
refused
.run_with(
(),
dbos::RunOptions {
workflow_id: Some(workflow_id),
..Default::default()
},
)
.await
.expect("the workflow failed");
let steps = reader(&db)
.await
.list_workflow_steps(workflow_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 call took a step id the step after it should have had",
);
dbos.shutdown().await;
}