use std::sync::Arc;
use std::time::Duration;
use dbos::sysdb::SystemDatabase;
use dbos::sysdb::postgres::{PostgresSystemDatabase, Settings};
use dbos::{Client, ClientConfig, Config, DBOS, Error, WorkflowHandle, WorkflowRef};
use dbos_test_support::{TestDatabase, test_database};
const DEADLINE: Duration = Duration::from_secs(60);
const APP_VERSION: &str = "1.0.0";
fn config(app_name: &str, db: &TestDatabase) -> Config {
Config {
migrate: false,
app_version: Some(APP_VERSION.to_owned()),
outcome_poll_interval: Some(Duration::from_millis(50)),
..Config::new(app_name, db.url())
}
}
async fn reader(db: &TestDatabase) -> PostgresSystemDatabase {
PostgresSystemDatabase::from_pool(db.pool().await, &Settings::default())
}
async fn steps(reader: &PostgresSystemDatabase, workflow_id: &str) -> Vec<(i32, String)> {
reader
.list_workflow_steps(workflow_id, true, None, None, None)
.await
.expect("read failed")
.into_iter()
.map(|step| (step.step_id, step.step_name))
.collect()
}
fn register_race(dbos: &DBOS, gate: &Arc<tokio::sync::Notify>) -> WorkflowRef<(), String> {
let counter = dbos
.register_workflow("counter", |n: u32| async move { Ok::<_, Error>(n + 1) })
.unwrap();
let namer = dbos
.register_workflow("namer", {
let gate = Arc::clone(gate);
move |()| {
let gate = Arc::clone(&gate);
async move {
gate.notified().await;
Ok::<_, Error>("namer".to_owned())
}
}
})
.unwrap();
dbos.register_workflow("parent", {
move |()| {
let (counter, namer) = (counter.clone(), namer.clone());
async move {
let count = counter.start(3).await?;
let name = namer.start(()).await?;
let won = dbos::select_workflow! {
n = count => format!("counter: {}", n?),
s = name => format!("namer: {}", s?),
}?;
Ok::<_, Error>(won)
}
}
})
.unwrap()
}
fn race_steps() -> [(i32, String); 4] {
[
(0, "counter".to_owned()),
(1, "namer".to_owned()),
(2, "DBOS.selectWorkflow".to_owned()),
(3, "DBOS.getResult".to_owned()),
]
}
#[tokio::test]
async fn select_workflow_runs_the_winners_arm_and_awaits_nobody_else() {
let db = test_database().await;
let dbos = DBOS::new(config("select-macro-app", &db));
let reader = reader(&db).await;
let gate = Arc::new(tokio::sync::Notify::new());
let parent = register_race(&dbos, &gate);
dbos.launch().await.expect("launch failed");
let handle = parent.start(()).await.expect("start failed");
let workflow_id = handle.workflow_id().to_owned();
let won = tokio::time::timeout(DEADLINE, handle.result())
.await
.expect("the parent never finished")
.expect("the parent failed");
assert_eq!(
won, "counter: 4",
"the arm that ran is the winner's, and it holds the winner's own typed result"
);
assert_eq!(
steps(&reader, &workflow_id).await,
race_steps(),
"the two launches, the wait the macro expands to, and the winner's result alone"
);
gate.notify_waiters();
dbos.shutdown().await;
}
#[tokio::test]
async fn a_replayed_select_takes_the_same_arm() {
let db = test_database().await;
let dbos = DBOS::new(config("select-replay-app", &db));
let reader = reader(&db).await;
let gate = Arc::new(tokio::sync::Notify::new());
let parent = register_race(&dbos, &gate);
dbos.launch().await.expect("launch failed");
let handle = parent.start(()).await.expect("start failed");
let workflow_id = handle.workflow_id().to_owned();
let won = tokio::time::timeout(DEADLINE, handle.result())
.await
.expect("the parent never finished")
.expect("the parent failed");
let forked: WorkflowHandle<String> = dbos.fork(&workflow_id, 3).await.expect("fork failed");
let forked_id = forked.workflow_id().to_owned();
let again = tokio::time::timeout(DEADLINE, forked.result())
.await
.expect("the fork never finished")
.expect("the fork failed");
assert_eq!(again, won, "the replay took the arm the run took");
assert_eq!(
steps(&reader, &forked_id).await,
race_steps(),
"the same sequence, with the launches and the wait inherited"
);
gate.notify_waiters();
dbos.shutdown().await;
}
#[tokio::test]
async fn join_workflows_returns_every_result_in_source_order() {
let db = test_database().await;
let dbos = DBOS::new(config("join-macro-app", &db));
let reader = reader(&db).await;
let counter = dbos
.register_workflow("counter", |n: u32| async move { Ok::<_, Error>(n + 1) })
.unwrap();
let namer = dbos
.register_workflow(
"namer",
|()| async move { Ok::<_, Error>("namer".to_owned()) },
)
.unwrap();
let parent = dbos
.register_workflow("parent", {
let (counter, namer) = (counter.clone(), namer.clone());
move |()| {
let (counter, namer) = (counter.clone(), namer.clone());
async move {
let count = counter.start(3).await?;
let name = namer.start(()).await?;
let (count, name) = dbos::join_workflows!(count, name)?;
Ok::<_, Error>(format!("{name} counted to {count}"))
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let handle = parent.start(()).await.expect("start failed");
let workflow_id = handle.workflow_id().to_owned();
let both = tokio::time::timeout(DEADLINE, handle.result())
.await
.expect("the parent never finished")
.expect("the parent failed");
assert_eq!(
both, "namer counted to 4",
"both results come back, each with its own type"
);
assert_eq!(
steps(&reader, &workflow_id).await,
[
(0, "counter".to_owned()),
(1, "namer".to_owned()),
(2, "DBOS.getResult".to_owned()),
(3, "DBOS.getResult".to_owned()),
],
"the two launches and a result read per handle in source order, the wait recording nothing"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn an_instance_before_the_semicolon_waits_from_outside_a_workflow() {
let db = test_database().await;
let dbos = DBOS::new(config("instance-macro-app", &db));
let counter = dbos
.register_workflow("counter", |n: u32| async move { Ok::<_, Error>(n + 1) })
.unwrap();
let labeler = dbos
.register_workflow("labeler", |()| async move {
Ok::<_, Error>("labeler".to_owned())
})
.unwrap();
let gate = Arc::new(tokio::sync::Notify::new());
let namer = dbos
.register_workflow("namer", {
let gate = Arc::clone(&gate);
move |()| {
let gate = Arc::clone(&gate);
async move {
gate.notified().await;
Ok::<_, Error>("namer".to_owned())
}
}
})
.unwrap();
dbos.launch().await.expect("launch failed");
let client = Client::connect(ClientConfig {
app_name: Some("instance-macro-app".to_owned()),
outcome_poll_interval: Some(Duration::from_millis(50)),
..ClientConfig::new(db.url())
})
.await
.expect("connect failed");
let count = counter.start(3).await.expect("start failed");
let name = namer.start(()).await.expect("start failed");
let won: dbos::Result<String> = dbos::select_workflow! { &dbos;
n = count => format!("counter: {}", n.expect("the counter failed")),
s = name => format!("namer: {}", s.expect("the namer failed")),
};
assert_eq!(
won.expect("the instance's race failed"),
"counter: 4",
"the instance form answers with the winner's own result, as the ambient form does"
);
let count = counter.start(4).await.expect("start failed");
let name = namer.start(()).await.expect("start failed");
let won: dbos::Result<String> = dbos::select_workflow! { client;
n = count => format!("counter: {}", n.expect("the counter failed")),
s = name => format!("namer: {}", s.expect("the namer failed")),
};
assert_eq!(
won.expect("the client's race failed"),
"counter: 5",
"a client races the same way, on the same expansion"
);
gate.notify_waiters();
let count = counter.start(9).await.expect("start failed");
let label = labeler.start(()).await.expect("start failed");
let joined: dbos::Result<(u32, String)> = tokio::time::timeout(DEADLINE, async {
dbos::join_workflows!(client; count, label)
})
.await
.expect("the client's join never resolved");
assert_eq!(
joined.expect("the client's join failed"),
(10, "labeler".to_owned())
);
let count = counter.start(19).await.expect("start failed");
let label = labeler.start(()).await.expect("start failed");
let joined: dbos::Result<(u32, String)> = tokio::time::timeout(DEADLINE, async {
dbos::join_workflows!(&dbos; count, label)
})
.await
.expect("the instance's join never resolved");
assert_eq!(
joined.expect("the instance's join failed"),
(20, "labeler".to_owned())
);
dbos.shutdown().await;
}