use std::marker::PhantomData;
use crate::checkpoint::{PendingStep, StepPlacement};
use crate::error::{DurableError, Error, Result};
use crate::serialization::{decode, encode};
use crate::sysdb::types::{Outcome, Timestamp, step_names};
pub enum Racing {
Replay(usize),
Fresh(Recording),
}
pub struct Recording {
placement: StepPlacement,
started_at: Timestamp,
}
pub struct Branches<E> {
identities: Vec<(String, Option<i32>)>,
failure: PhantomData<fn() -> E>,
}
impl<E> Default for Branches<E> {
fn default() -> Self {
Self::new()
}
}
impl<E> Branches<E> {
#[must_use]
pub fn new() -> Self {
Self {
identities: Vec::new(),
failure: PhantomData,
}
}
pub fn push<T>(&mut self, branch: &PendingStep<'_, T, E>) {
self.identities
.push((branch.name().to_owned(), branch.step_id()));
}
#[must_use]
pub fn len(&self) -> usize {
self.identities.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.identities.is_empty()
}
fn summarize(&self) -> String {
self.identities
.iter()
.enumerate()
.map(|(at, (name, step_id))| match step_id {
Some(step_id) => format!("{at}: {name} (step {step_id})"),
None => format!("{at}: {name} (uncheckpointed)"),
})
.collect::<Vec<_>>()
.join(", ")
}
}
pub async fn check_select<E: DurableError>(branches: &Branches<E>) -> Result<Racing, E> {
let placement = StepPlacement::here();
let started_at = Timestamp::now();
if let Some(executor) = placement.executor()
&& let Some(recorded) = placement
.check(executor.connection(), step_names::SELECT_STEP)
.await
.map_err(Error::lift)?
{
let winner: usize = decode(recorded.output.as_deref(), "the branch that won a select")?;
if winner >= branches.len() {
let (workflow_id, step_id) = placement.step().unwrap_or(("", 0));
return Err(Error::SystemDatabase(crate::sysdb::Error::UnexpectedStep {
workflow_id: workflow_id.to_owned(),
step_id,
expected: format!(
"a select over {} branches — {}",
branches.len(),
branches.summarize()
),
recorded: format!("a select won by branch {winner}, which no longer exists"),
}));
}
tracing::debug!(winner, "replaying select_step; the same branch wins again");
return Ok(Racing::Replay(winner));
}
Ok(Racing::Fresh(Recording {
placement,
started_at,
}))
}
pub async fn record_select<E: DurableError>(recording: Recording, winner: usize) -> Result<(), E> {
let Recording {
placement,
started_at,
} = recording;
let Some(executor) = placement.executor() else {
return Ok(());
};
let encoded = encode(&winner, "the branch that won a select")?;
placement
.record(
executor.connection(),
step_names::SELECT_STEP,
Outcome::Output(Some(&encoded)),
started_at,
)
.await
.map_err(Error::lift)
}
pub fn control_error<T, E>(slot: &mut Option<Result<T, E>>) -> Option<Error<E>> {
match slot {
Some(Err(failure)) if failure.control().is_some() => slot.take().and_then(Result::err),
_ => None,
}
}
#[cfg(all(test, feature = "macros"))]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use super::*;
use crate::context::Ctx;
use crate::step::step;
use crate::sysdb::types::{NewWorkflow, Submission};
use crate::{Config, DBOS};
async fn workflow(id: &str) -> (DBOS, dbos_test_support::TestDatabase) {
let db = dbos_test_support::test_database().await;
let dbos = DBOS::new(Config {
migrate: false,
app_version: Some("1.0.0".to_owned()),
..Config::new("select-test", db.url())
});
dbos.launch().await.expect("launch failed");
dbos.executor("test")
.expect("launched")
.sysdb()
.init_workflow(&NewWorkflow::new(id), None, Submission::Fresh, None)
.await
.expect("could not create the workflow row");
(dbos, db)
}
fn ctx(dbos: &DBOS, id: &str) -> Ctx {
Ctx::new(dbos.executor("test").expect("launched"), id, None)
}
async fn steps(dbos: &DBOS, id: &str) -> Vec<(i32, String)> {
dbos.executor("test")
.expect("launched")
.sysdb()
.list_workflow_steps(id, true, None, None, None)
.await
.expect("read failed")
.into_iter()
.map(|step| (step.step_id, step.step_name))
.collect()
}
fn never(name: &'static str) -> PendingStep<'static, u32> {
step(name, || async {
std::future::pending::<()>().await;
Ok::<_, crate::Error>(0u32)
})
}
#[tokio::test]
async fn a_race_runs_one_arm_and_the_loser_records_nothing() {
let (dbos, _db) = workflow("wf-macro").await;
let loser_ran = Arc::new(AtomicU32::new(0));
let answer: crate::Result<String> = Ctx::scope(ctx(&dbos, "wf-macro"), {
let loser_ran = Arc::clone(&loser_ran);
async move {
crate::select_step! {
slow = step("slow", {
let loser_ran = Arc::clone(&loser_ran);
move || {
let loser_ran = Arc::clone(&loser_ran);
async move {
loser_ran.fetch_add(1, Ordering::SeqCst);
std::future::pending::<()>().await;
Ok::<_, crate::Error>(1u32)
}
}
}) => format!("the counter won with {}", slow?),
quick = step("quick", || async {
Ok::<_, crate::Error>("hello".to_owned())
}) => format!("the namer won with {}", quick?)
}
}
})
.await;
assert_eq!(answer.unwrap(), "the namer won with hello");
assert_eq!(
loser_ran.load(Ordering::SeqCst),
1,
"the loser was polled and started before the winner finished"
);
assert_eq!(
steps(&dbos, "wf-macro").await,
[(1, "quick".to_owned()), (2, "DBOS.selectStep".to_owned())],
"the winner keeps its build-order id, the loser records nothing"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn branches_of_two_error_types_agree_through_map_error() {
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("the gateway refused")]
struct Refused;
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("charging failed: {0}")]
struct Charging(String);
let (dbos, _db) = workflow("wf-two-channels").await;
let answer: crate::Result<String, Charging> =
Ctx::scope(ctx(&dbos, "wf-two-channels"), async {
crate::select_step! {
refused = step("refused", || async { Err::<u32, Error<Refused>>(Refused.into()) })
.map_error(|refused: Refused| Charging(refused.to_string())) => {
format!("refused: {}", refused.unwrap_err())
}
slow = step("slow", || async {
std::future::pending::<()>().await;
Err::<u32, Error<Charging>>(Charging("never".to_owned()).into())
}) => format!("slow: {}", slow.unwrap_err())
}
})
.await;
assert_eq!(
answer.unwrap(),
"refused: charging failed: the gateway refused",
"the arm sees the branch's error converted into the channel the race agreed on"
);
assert_eq!(
steps(&dbos, "wf-two-channels").await,
[(0, "refused".to_owned()), (2, "DBOS.selectStep".to_owned())],
"the winner's row and the select's, with the loser's id spent and unrecorded"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_replayed_race_takes_the_recorded_branch_and_polls_no_other() {
let (dbos, _db) = workflow("wf-macro-replay").await;
let loser_ran = Arc::new(AtomicU32::new(0));
let race = |loser_ran: Arc<AtomicU32>| async move {
crate::select_step! {
quick = step("quick", || async { Ok::<_, crate::Error>(7u32) }) => {
format!("quick: {}", quick?)
}
slow = step("slow", {
let loser_ran = Arc::clone(&loser_ran);
move || {
let loser_ran = Arc::clone(&loser_ran);
async move {
loser_ran.fetch_add(1, Ordering::SeqCst);
std::future::pending::<()>().await;
Ok::<_, crate::Error>("slow".to_owned())
}
}
}) => format!("slow: {}", slow?)
}
};
let first: crate::Result<String> =
Ctx::scope(ctx(&dbos, "wf-macro-replay"), race(Arc::clone(&loser_ran))).await;
assert_eq!(first.unwrap(), "quick: 7");
let started_once = loser_ran.load(Ordering::SeqCst);
let again: crate::Result<String> =
Ctx::scope(ctx(&dbos, "wf-macro-replay"), race(Arc::clone(&loser_ran))).await;
assert_eq!(again.unwrap(), "quick: 7", "the replay takes the same arm");
assert_eq!(
loser_ran.load(Ordering::SeqCst),
started_once,
"the loser recorded nothing the first time, so a replay must not run it now"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_race_re_run_in_the_window_replays_the_branch_that_recorded() {
let (dbos, _db) = workflow("wf-window").await;
let executor = dbos.executor("test").expect("launched");
let quick_ran = Arc::new(AtomicU32::new(0));
executor
.sysdb()
.record_step(
"wf-window",
1,
"quick",
Outcome::Output(Some(
&encode::<_, crate::Error>(&"hello".to_owned(), "the winner").expect("encodes"),
)),
Some(executor.connection().serializer().name()),
None,
)
.await
.expect("could not plant the winner's row");
let answer: crate::Result<String> = Ctx::scope(ctx(&dbos, "wf-window"), {
let quick_ran = Arc::clone(&quick_ran);
async move {
crate::select_step! {
slow = never("slow") => format!("slow: {}", slow?),
quick = step("quick", {
let quick_ran = Arc::clone(&quick_ran);
move || {
let quick_ran = Arc::clone(&quick_ran);
async move {
quick_ran.fetch_add(1, Ordering::SeqCst);
Ok::<_, crate::Error>("hello".to_owned())
}
}
}) => format!("the namer won with {}", quick?)
}
}
})
.await;
assert_eq!(answer.unwrap(), "the namer won with hello");
assert_eq!(
quick_ran.load(Ordering::SeqCst),
0,
"the branch that recorded replays from its row; the window costs no second execution"
);
assert_eq!(
steps(&dbos, "wf-window").await,
[(1, "quick".to_owned()), (2, "DBOS.selectStep".to_owned())],
"the winner's row, and the select row that was missing"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_losing_sleep_leaves_a_row_for_a_wait_it_never_finished() {
let (dbos, _db) = workflow("wf-losing-sleep").await;
let answer: crate::Result<String> = Ctx::scope(ctx(&dbos, "wf-losing-sleep"), async {
crate::select_step! {
timeout = crate::sleep(std::time::Duration::from_secs(30)) => {
timeout?;
"timed out".to_owned()
}
quick = step("quick", || async { Ok::<_, crate::Error>("hello".to_owned()) })
=> format!("the namer won with {}", quick?)
}
})
.await;
assert_eq!(answer.unwrap(), "the namer won with hello");
assert_eq!(
steps(&dbos, "wf-losing-sleep").await,
[
(0, "DBOS.sleep".to_owned()),
(1, "quick".to_owned()),
(2, "DBOS.selectStep".to_owned())
],
"the sleep lost and still recorded, which is why a branch row is not a winner"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_recorded_winner_outside_the_current_set_is_reported() {
let (dbos, _db) = workflow("wf-shrunk").await;
let first: crate::Result<u32> = Ctx::scope(ctx(&dbos, "wf-shrunk"), async {
crate::select_step! {
b = never("b") => b?,
c = never("c") => c?,
a = step("a", || async { Ok::<_, crate::Error>(1u32) }) => a?
}
})
.await;
assert_eq!(first.unwrap(), 1, "the branch built third finished first");
let shrunk: crate::Result<u32> = Ctx::scope(ctx(&dbos, "wf-shrunk"), async {
let _third = step("a", || async { Ok::<_, crate::Error>(1u32) });
crate::select_step! {
b = step("b", || async { Ok::<_, crate::Error>(2u32) }) => b?,
c = step("c", || async { Ok::<_, crate::Error>(3u32) }) => c?
}
})
.await;
match shrunk {
Err(crate::Error::SystemDatabase(crate::sysdb::Error::UnexpectedStep {
expected,
recorded,
..
})) => {
assert!(recorded.contains("branch 2"), "{recorded}");
assert!(
expected.contains("0: b (step 1), 1: c (step 2)"),
"{expected}"
);
}
other => panic!("a stale winner must be reported, got {other:?}"),
}
dbos.shutdown().await;
}
#[tokio::test]
async fn outside_a_workflow_the_race_is_plain() {
let answer: crate::Result<u32> = async {
crate::select_step! {
quick = step("quick", || async { Ok::<_, crate::Error>(1u32) }) => quick?,
slow = never("slow") => slow?
}
}
.await;
assert_eq!(answer.unwrap(), 1);
}
#[tokio::test]
async fn inside_a_step_body_the_race_is_plain() {
let (dbos, _db) = workflow("wf-in-step").await;
let answer: crate::Result<u32> = Ctx::scope(ctx(&dbos, "wf-in-step"), async {
step("outer", || async {
crate::select_step! {
quick = step("inner-quick", || async { Ok::<_, crate::Error>(5u32) }) => quick?,
slow = never("inner-slow") => slow?
}
})
.await
})
.await;
assert_eq!(answer.unwrap(), 5);
assert_eq!(
steps(&dbos, "wf-in-step").await,
[(0, "outer".to_owned())],
"the step's own row stands for the race inside it"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_race_wider_than_any_hand_written_arity() {
let (dbos, _db) = workflow("wf-macro-wide").await;
let answer: crate::Result<u32> = Ctx::scope(ctx(&dbos, "wf-macro-wide"), async {
crate::select_step! {
a = never("a") => a?,
b = never("b") => b?,
c = never("c") => c?,
d = never("d") => d?,
e = never("e") => e?,
f = never("f") => f?,
g = never("g") => g?,
h = never("h") => h?,
i = never("i") => i?,
j = step("j", || async { Ok::<_, crate::Error>(10u32) }) => j?,
}
})
.await;
assert_eq!(answer.unwrap(), 10, "the tenth branch finished first");
assert_eq!(
steps(&dbos, "wf-macro-wide").await,
[(9, "j".to_owned()), (10, "DBOS.selectStep".to_owned())],
"nine losers spend their ids and record nothing"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_block_arm_ends_itself_like_a_match_arm() {
let answer: crate::Result<u32> = async {
crate::select_step! {
quick = step("quick", || async { Ok::<_, crate::Error>(1u32) }) => {
let won = quick?;
won + 10
}
slow = never("slow") => match slow {
Ok(value) => value + 1,
Err(_) => 0,
}
}
}
.await;
assert_eq!(answer.unwrap(), 11);
}
#[tokio::test]
async fn an_engine_channel_call_joins_a_race_in_the_applications_channel() {
#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)]
#[error("the application failed")]
struct AppError;
let (dbos, _db) = workflow("wf-lifted").await;
let filter = crate::sysdb::types::WorkflowFilter::default();
let listed: crate::Result<bool, AppError> = Ctx::scope(ctx(&dbos, "wf-lifted"), async {
crate::select_step! {
never = crate::step("never", || async {
std::future::pending::<()>().await;
Ok::<bool, crate::Error<AppError>>(false)
}) => never?,
rows = dbos.list_workflows(&filter).lift() => !rows?.is_empty()
}
})
.await;
assert!(
listed.unwrap(),
"the listing won and saw this workflow's row"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn the_error_type_is_inferred_from_the_branches() {
let (dbos, _db) = workflow("wf-macro-inferred").await;
let answer = Ctx::scope(ctx(&dbos, "wf-macro-inferred"), async {
crate::select_step! {
counted = never("counted") => counted.is_ok(),
named = step("named", || async {
Ok::<_, crate::Error>("hello".to_owned())
}) => named.is_ok(),
}
})
.await;
assert!(answer.unwrap(), "the branch that finished first succeeded");
dbos.shutdown().await;
}
}