use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
use tokio_util::sync::CancellationToken;
use crate::instance::Executor;
use crate::sysdb::types::Timestamp;
tokio::task_local! {
static CURRENT: Ctx;
}
#[derive(Clone)]
pub(crate) struct Ctx {
executor: Arc<Executor>,
workflow: Arc<WorkflowState>,
step: Option<StepScope>,
}
#[derive(Clone, Debug)]
pub(crate) struct StepScope {
marker: StepMarker,
status: StepStatus,
cancellation: CancellationToken,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StepStatus {
pub(crate) step_id: i32,
pub(crate) current_attempt: u32,
pub(crate) max_attempts: u32,
}
impl StepStatus {
pub fn step_id(&self) -> i32 {
self.step_id
}
pub fn current_attempt(&self) -> u32 {
self.current_attempt
}
pub fn max_attempts(&self) -> u32 {
self.max_attempts
}
}
#[cfg(test)]
impl StepStatus {
pub(crate) fn first(step_id: i32) -> Self {
Self {
step_id,
current_attempt: 1,
max_attempts: 1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct StepMarker(u64);
static NEXT_STEP_MARKER: AtomicU64 = AtomicU64::new(0);
struct WorkflowState {
workflow_id: String,
deadline: Option<Timestamp>,
next_step_id: AtomicI32,
}
impl WorkflowState {
fn next_step_id(&self) -> i32 {
self.next_step_id.fetch_add(1, Ordering::Relaxed)
}
}
impl Ctx {
pub(crate) fn new(
executor: Arc<Executor>,
workflow_id: impl Into<String>,
deadline: Option<Timestamp>,
) -> Self {
Self {
executor,
workflow: Arc::new(WorkflowState {
workflow_id: workflow_id.into(),
deadline,
next_step_id: AtomicI32::new(0),
}),
step: None,
}
}
pub(crate) fn current() -> Option<Ctx> {
CURRENT.try_with(Ctx::clone).ok()
}
pub(crate) fn with_current<R>(f: impl FnOnce(Option<&Ctx>) -> R) -> R {
let mut f = Some(f);
match CURRENT.try_with(|ctx| f.take().expect("the closure runs once")(Some(ctx))) {
Ok(answer) => answer,
Err(_) => f.take().expect("the closure runs once")(None),
}
}
pub(crate) fn workflow_id(&self) -> &str {
&self.workflow.workflow_id
}
pub(crate) fn step_id(&self) -> Option<i32> {
self.step.as_ref().map(|step| step.status.step_id)
}
pub(crate) fn step_status(&self) -> Option<StepStatus> {
self.step.as_ref().map(|step| step.status)
}
pub(crate) fn is_same_execution(&self, other: &Ctx) -> bool {
Arc::ptr_eq(&self.workflow, &other.workflow)
}
pub(crate) fn deadline(&self) -> Option<Timestamp> {
self.workflow.deadline
}
pub(crate) async fn scope<F: Future>(ctx: Ctx, future: F) -> F::Output {
CURRENT.scope(ctx, future).await
}
pub(crate) fn next_step_id(&self) -> i32 {
self.workflow.next_step_id()
}
pub(crate) fn executor(&self) -> &Arc<Executor> {
&self.executor
}
pub(crate) fn in_step(&self) -> bool {
self.step.is_some()
}
pub(crate) fn step_marker(&self) -> Option<StepMarker> {
self.step.as_ref().map(|step| step.marker)
}
pub(crate) async fn in_step_scope<F: Future>(
&self,
cancellation: CancellationToken,
status: StepStatus,
body: F,
) -> F::Output {
let scoped = Ctx {
executor: Arc::clone(&self.executor),
workflow: Arc::clone(&self.workflow),
step: Some(StepScope {
marker: StepMarker(NEXT_STEP_MARKER.fetch_add(1, Ordering::Relaxed)),
status,
cancellation,
}),
};
CURRENT.scope(scoped, body).await
}
pub(crate) fn cancellation(&self) -> CancellationToken {
self.step
.as_ref()
.map(|step| step.cancellation.clone())
.unwrap_or_default()
}
}
pub fn workflow_id() -> Option<String> {
Ctx::with_current(|ctx| ctx.map(|ctx| ctx.workflow_id().to_owned()))
}
pub fn step_id() -> Option<i32> {
Ctx::with_current(|ctx| ctx.and_then(Ctx::step_id))
}
pub fn step_status() -> Option<StepStatus> {
Ctx::with_current(|ctx| ctx.and_then(Ctx::step_status))
}
pub fn cancellation_token() -> CancellationToken {
Ctx::with_current(|ctx| ctx.map(Ctx::cancellation).unwrap_or_default())
}
impl std::fmt::Debug for Ctx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ctx")
.field("workflow_id", &self.workflow.workflow_id)
.field(
"steps_taken",
&self.workflow.next_step_id.load(Ordering::Relaxed),
)
.field("step", &self.step)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state() -> WorkflowState {
WorkflowState {
workflow_id: "wf-1".to_owned(),
deadline: None,
next_step_id: AtomicI32::new(0),
}
}
#[test]
fn step_ids_are_zero_based() {
let state = state();
assert_eq!(
state.next_step_id(),
0,
"Go, TypeScript and Java all number the first step 0"
);
assert_eq!(state.next_step_id(), 1);
assert_eq!(state.next_step_id(), 2);
}
#[test]
fn step_ids_are_never_reused_across_threads() {
let state = Arc::new(state());
let handles: Vec<_> = (0..8)
.map(|_| {
let state = Arc::clone(&state);
std::thread::spawn(move || {
(0..100).map(|_| state.next_step_id()).collect::<Vec<_>>()
})
})
.collect();
let mut ids: Vec<i32> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
ids.sort_unstable();
assert_eq!(
ids,
(0..800).collect::<Vec<_>>(),
"800 allocations, 800 distinct ids"
);
}
#[tokio::test]
async fn there_is_no_context_outside_a_workflow() {
assert!(Ctx::current().is_none());
}
async fn ctx(
workflow_id: &str,
deadline: Option<Timestamp>,
) -> (Ctx, crate::DBOS, dbos_test_support::TestDatabase) {
let db = dbos_test_support::test_database().await;
let dbos = crate::DBOS::new(crate::Config {
migrate: false,
app_version: Some("1.0.0".to_owned()),
..crate::Config::new("ctx-test", db.url())
});
dbos.launch().await.expect("launch failed");
let ctx = Ctx::new(
dbos.executor("test").expect("launched"),
workflow_id,
deadline,
);
(ctx, dbos, db)
}
#[tokio::test]
async fn a_context_is_ambient_within_its_scope_and_gone_outside_it() {
let (ctx, dbos, _db) = ctx("wf-42", None).await;
assert!(Ctx::current().is_none(), "nothing before");
Ctx::scope(ctx, async {
assert_eq!(Ctx::current().expect("inside").workflow_id(), "wf-42");
tokio::task::yield_now().await;
assert_eq!(
Ctx::current().expect("still inside").workflow_id(),
"wf-42",
"the context survives an await, which is the whole point of a task-local"
);
})
.await;
assert!(Ctx::current().is_none(), "nothing after");
dbos.shutdown().await;
}
#[tokio::test]
async fn the_deadline_travels_with_the_context_and_survives_a_step_scope() {
let deadline = Timestamp::from_epoch_ms(1_700_000_000_000);
let (ctx, dbos, _db) = ctx("wf-deadline", Some(deadline)).await;
let inner = ctx.clone();
Ctx::scope(ctx, async move {
assert_eq!(
Ctx::current().expect("inside").deadline(),
Some(deadline),
"the body reads the deadline its row carries"
);
inner
.in_step_scope(CancellationToken::new(), StepStatus::first(0), async {
assert_eq!(
Ctx::current().expect("inside a step").deadline(),
Some(deadline),
"a step's rebinding shares the workflow state, so the deadline comes with it"
);
})
.await;
})
.await;
dbos.shutdown().await;
}
#[tokio::test]
async fn a_sibling_that_finishes_does_not_end_this_step() {
let (ctx, dbos, _db) = ctx("wf-siblings", None).await;
let (inside, wait_for_inside) = tokio::sync::oneshot::channel();
let (done, wait_for_done) = tokio::sync::oneshot::channel();
let long = ctx.in_step_scope(CancellationToken::new(), StepStatus::first(0), async move {
inside.send(()).expect("the sibling is waiting on this");
wait_for_done.await.expect("the sibling ran to completion");
Ctx::current().expect("inside a step").in_step()
});
let short = async {
wait_for_inside
.await
.expect("the long step reached its body");
ctx.in_step_scope(
CancellationToken::new(),
StepStatus::first(0),
std::future::ready(()),
)
.await;
done.send(()).expect("the long step is waiting on this");
};
let (still_inside, ()) = tokio::join!(long, short);
assert!(
still_inside,
"a sibling finishing must not tell a step it has left its own body"
);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_running_step_does_not_make_the_workflow_proper_look_nested() {
let (ctx, dbos, _db) = ctx("wf-proper", None).await;
let (inside, wait_for_inside) = tokio::sync::oneshot::channel();
let (looked, wait_for_look) = tokio::sync::oneshot::channel();
let stepping = ctx.clone();
Ctx::scope(ctx, async move {
let held = stepping.in_step_scope(
CancellationToken::new(),
StepStatus::first(0),
async move {
inside
.send(())
.expect("the workflow body is waiting on this");
wait_for_look.await.expect("the workflow body looked");
},
);
let proper = async {
wait_for_inside.await.expect("the step reached its body");
let seen = Ctx::current().expect("inside the workflow").in_step();
looked.send(()).expect("the step is waiting on this");
seen
};
let ((), seen) = tokio::join!(held, proper);
assert!(
!seen,
"the workflow body is not inside the step it is waiting on"
);
})
.await;
dbos.shutdown().await;
}
#[tokio::test]
async fn the_context_does_not_cross_a_spawn() {
let (ctx, dbos, _db) = ctx("wf-42", None).await;
let spawned_saw = Ctx::scope(ctx, async {
tokio::spawn(async { Ctx::current().map(|c| c.workflow_id().to_owned()) })
.await
.expect("the spawned task panicked")
})
.await;
assert_eq!(spawned_saw, None);
dbos.shutdown().await;
}
#[tokio::test]
async fn a_clone_shares_one_step_counter() {
let (ctx, dbos, _db) = ctx("wf-42", None).await;
let clone = ctx.clone();
assert_eq!(ctx.next_step_id(), 0);
assert_eq!(
clone.next_step_id(),
1,
"a clone is the same workflow, not a second one"
);
assert_eq!(ctx.next_step_id(), 2);
dbos.shutdown().await;
}
}