use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use dbos::{Config, DBOS, StepOptions, StepStatus};
use std::time::Duration;
use dbos_test_support::{TestDatabase, test_database};
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()),
..Config::new(app_name, db.url())
}
}
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("failed on purpose")]
struct Flaky;
async fn reads_id(_: ()) -> dbos::Result<String> {
Ok(dbos::workflow_id().expect("inside a workflow"))
}
type StepIdTrace = (
Option<i32>,
Option<i32>,
Option<i32>,
Option<i32>,
Option<i32>,
);
async fn reads_step_ids(_: ()) -> dbos::Result<StepIdTrace> {
let before = dbos::step_id();
let first = dbos::step("first", || async { Ok(dbos::step_id()) }).await?;
let between = dbos::step_id();
let second = dbos::step("second", || async { Ok(dbos::step_id()) }).await?;
let after = dbos::step_id();
Ok((before, first, between, second, after))
}
async fn reads_both(_: ()) -> dbos::Result<(Option<String>, Option<i32>)> {
dbos::step("inner", || async {
Ok((dbos::workflow_id(), dbos::step_id()))
})
.await
}
#[tokio::test]
async fn a_workflow_reads_its_own_id() {
let db = test_database().await;
let dbos = DBOS::new(config("own-id-app", &db));
let workflow = dbos.register_workflow("reads_id", reads_id).unwrap();
dbos.launch().await.expect("launch failed");
let handle = workflow.start(()).await.expect("start failed");
let id = handle.workflow_id().to_owned();
let reported = handle.result().await.expect("the workflow failed");
assert_eq!(reported, id, "a workflow's own id is the caller's id");
}
#[tokio::test]
async fn there_is_no_workflow_id_outside_a_workflow() {
assert_eq!(dbos::workflow_id(), None);
assert_eq!(dbos::step_id(), None);
}
#[tokio::test]
async fn step_ids_are_visible_inside_steps_and_nowhere_else() {
let db = test_database().await;
let dbos = DBOS::new(config("step-id-app", &db));
let workflow = dbos
.register_workflow("reads_step_ids", reads_step_ids)
.unwrap();
dbos.launch().await.expect("launch failed");
let (before, first, between, second, after) =
workflow.run(()).await.expect("the workflow failed");
assert_eq!(before, None, "the workflow body is not inside a step");
assert_eq!(first, Some(0), "step ids count from zero");
assert_eq!(between, None, "between two steps is inside neither");
assert_eq!(second, Some(1), "the next step takes the next id");
assert_eq!(after, None, "the last step's id does not leak out of it");
}
#[tokio::test]
async fn a_step_can_read_the_workflow_it_belongs_to() {
let db = test_database().await;
let dbos = DBOS::new(config("step-workflow-id-app", &db));
let workflow = dbos.register_workflow("reads_both", reads_both).unwrap();
dbos.launch().await.expect("launch failed");
let handle = workflow.start(()).await.expect("start failed");
let id = handle.workflow_id().to_owned();
let (seen_workflow, seen_step) = handle.result().await.expect("the workflow failed");
assert_eq!(seen_workflow.as_deref(), Some(id.as_str()));
assert_eq!(seen_step, Some(0));
}
#[tokio::test]
async fn every_attempt_of_a_step_sees_the_same_id() {
let db = test_database().await;
let dbos = DBOS::new(config("retry-step-id-app", &db));
let seen: Arc<std::sync::Mutex<Vec<Option<i32>>>> = Arc::default();
let attempts = Arc::new(AtomicU32::new(0));
let workflow = {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
dbos.register_workflow("retries", move |()| {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
async move {
dbos::step("first", || async { dbos::Result::<(), Flaky>::Ok(()) }).await?;
let options = StepOptions {
max_attempts: 3,
interval: Duration::from_millis(1),
max_interval: Duration::from_millis(5),
..Default::default()
};
dbos::step_with("flaky", options, || {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
async move {
seen.lock().unwrap().push(dbos::step_id());
if attempts.fetch_add(1, Ordering::SeqCst) < 2 {
Err(Flaky)?
}
Ok(())
}
})
.await?;
dbos::Result::<(), Flaky>::Ok(())
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
workflow.run(()).await.expect("the workflow failed");
let seen = seen.lock().unwrap().clone();
assert_eq!(seen.len(), 3, "the step should have taken three attempts");
assert!(
seen.iter().all(|id| *id == Some(1)),
"every attempt reports the same step id, got {seen:?}"
);
}
#[tokio::test]
async fn a_plain_step_reports_one_attempt_of_one() {
let db = test_database().await;
let dbos = DBOS::new(config("plain-status-app", &db));
let seen: Arc<std::sync::Mutex<Option<StepStatus>>> = Arc::default();
let workflow = {
let seen = Arc::clone(&seen);
dbos.register_workflow("plain", move |()| {
let seen = Arc::clone(&seen);
async move {
dbos::step("only", || async {
*seen.lock().unwrap() = dbos::step_status();
dbos::Result::<(), Flaky>::Ok(())
})
.await
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
workflow.run(()).await.expect("the workflow failed");
let status = seen.lock().unwrap().expect("inside a step");
assert_eq!(
(
status.step_id(),
status.current_attempt(),
status.max_attempts()
),
(0, 1, 1)
);
}
#[tokio::test]
async fn the_attempt_number_counts_from_one_and_the_id_holds_still() {
let db = test_database().await;
let dbos = DBOS::new(config("status-retry-app", &db));
let seen: Arc<std::sync::Mutex<Vec<StepStatus>>> = Arc::default();
let attempts = Arc::new(AtomicU32::new(0));
let workflow = {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
dbos.register_workflow("retries", move |()| {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
async move {
dbos::step("first", || async { dbos::Result::<(), Flaky>::Ok(()) }).await?;
let options = StepOptions {
max_attempts: 3,
interval: Duration::from_millis(1),
max_interval: Duration::from_millis(5),
..Default::default()
};
dbos::step_with("flaky", options, || {
let seen = Arc::clone(&seen);
let attempts = Arc::clone(&attempts);
async move {
seen.lock()
.unwrap()
.push(dbos::step_status().expect("inside a step"));
if attempts.fetch_add(1, Ordering::SeqCst) < 2 {
Err(Flaky)?
}
Ok(())
}
})
.await?;
dbos::Result::<(), Flaky>::Ok(())
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
workflow.run(()).await.expect("the workflow failed");
let seen: Vec<(i32, u32, u32)> = seen
.lock()
.unwrap()
.iter()
.map(|s| (s.step_id(), s.current_attempt(), s.max_attempts()))
.collect();
assert_eq!(
seen,
vec![(1, 1, 3), (1, 2, 3), (1, 3, 3)],
"the attempt should count 1..=3 while the id stays put"
);
}
#[tokio::test]
async fn there_is_no_step_status_outside_a_step() {
assert_eq!(dbos::step_status(), None);
let db = test_database().await;
let dbos = DBOS::new(config("status-gap-app", &db));
let workflow = dbos
.register_workflow("gap", |()| async move {
let before = dbos::step_status().is_none();
dbos::step("only", || async { dbos::Result::<(), Flaky>::Ok(()) }).await?;
dbos::Result::<_, Flaky>::Ok((before, dbos::step_status().is_none()))
})
.unwrap();
dbos.launch().await.expect("launch failed");
let (before, after) = workflow.run(()).await.expect("the workflow failed");
assert!(before, "the workflow body is not inside a step");
assert!(after, "a finished step's status does not leak out of it");
}
type NestedTrace = (Option<StepStatus>, Option<StepStatus>, Option<i32>);
#[tokio::test]
async fn a_nested_step_reports_the_step_that_encloses_it() {
let db = test_database().await;
let dbos = DBOS::new(config("nested-status-app", &db));
let attempts = Arc::new(AtomicU32::new(0));
let seen: Arc<std::sync::Mutex<Vec<NestedTrace>>> = Arc::default();
let workflow = {
let attempts = Arc::clone(&attempts);
let seen = Arc::clone(&seen);
dbos.register_workflow("nested", move |()| {
let attempts = Arc::clone(&attempts);
let seen = Arc::clone(&seen);
async move {
dbos::step("first", || async { dbos::Result::<(), Flaky>::Ok(()) }).await?;
dbos::step_with(
"outer",
StepOptions {
max_attempts: 2,
interval: Duration::from_millis(1),
..StepOptions::default()
},
move || {
let attempts = Arc::clone(&attempts);
let seen = Arc::clone(&seen);
async move {
let outer = dbos::step_status();
dbos::step("inner", || async {
seen.lock().unwrap().push((
outer,
dbos::step_status(),
dbos::step_id(),
));
dbos::Result::<(), Flaky>::Ok(())
})
.await?;
match attempts.fetch_add(1, Ordering::SeqCst) {
0 => Err(Flaky)?,
_ => dbos::Result::<(), Flaky>::Ok(()),
}
}
},
)
.await
}
})
.unwrap()
};
dbos.launch().await.expect("launch failed");
workflow.run(()).await.expect("the workflow failed");
let seen = seen.lock().unwrap();
let observed: Vec<_> = seen
.iter()
.map(|(outer, inner, step_id)| {
let outer = outer.expect("inside the enclosing step");
let inner = inner.expect("a nested step is still inside the enclosing one");
assert_eq!(
inner, outer,
"a nested step should report the enclosing step's status, whole"
);
(
inner.step_id(),
inner.current_attempt(),
inner.max_attempts(),
*step_id,
)
})
.collect();
assert_eq!(
observed,
vec![(1, 1, 2, Some(1)), (1, 2, 2, Some(1))],
"the nested body should follow the enclosing step across its two attempts, id held still"
);
}
#[tokio::test]
async fn a_cancellation_token_outside_a_step_never_fires() {
const QUIET: Duration = Duration::from_millis(50);
async fn stayed_quiet(token: dbos::CancellationToken) -> bool {
tokio::time::timeout(QUIET, token.cancelled())
.await
.is_err()
}
assert!(
stayed_quiet(dbos::cancellation_token()).await,
"outside a workflow there is no attempt to abandon"
);
let db = test_database().await;
let dbos = DBOS::new(config("quiet-token-app", &db));
let workflow = dbos
.register_workflow("quiet", |()| async move {
let body = dbos::cancellation_token();
let abandoned = dbos::step_with(
"times-out",
StepOptions {
timeout: Some(Duration::from_millis(20)),
..StepOptions::default()
},
|| async {
std::future::pending::<()>().await;
dbos::Result::<(), Flaky>::Ok(())
},
)
.await;
assert!(
abandoned.is_err(),
"the step should have blown its deadline"
);
dbos::Result::<_, Flaky>::Ok(stayed_quiet(body).await)
})
.unwrap();
dbos.launch().await.expect("launch failed");
assert!(
workflow.run(()).await.expect("the workflow failed"),
"the workflow body's token should outlive an abandoned step of its own, unfired"
);
dbos.shutdown().await;
}