use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, AtomicU64, AtomicUsize, Ordering};
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use crate::instance::Executor;
use crate::serialization::PayloadEncoding;
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,
owner_xid: String,
deadline: Option<Timestamp>,
encoding: PayloadEncoding,
next_step_id: AtomicI32,
starts: Arc<Starts>,
}
impl WorkflowState {
fn next_step_id(&self) -> i32 {
self.next_step_id.fetch_add(1, Ordering::Relaxed)
}
fn peek_step_id(&self) -> i32 {
self.next_step_id.load(Ordering::Relaxed)
}
fn claim_step_id(&self, step_id: i32) -> bool {
self.next_step_id
.compare_exchange(step_id, step_id + 1, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
}
}
#[derive(Debug, Default)]
pub(crate) struct Starts {
in_flight: AtomicUsize,
none_left: Notify,
}
impl Starts {
pub(crate) fn token(self: &Arc<Self>) -> StartToken {
self.in_flight.fetch_add(1, Ordering::AcqRel);
StartToken(Arc::clone(self))
}
pub(crate) async fn drained(&self) {
loop {
let mut notified = std::pin::pin!(self.none_left.notified());
notified.as_mut().enable();
if self.in_flight.load(Ordering::Acquire) == 0 {
return;
}
notified.await;
}
}
}
#[derive(Debug)]
pub(crate) struct StartToken(Arc<Starts>);
impl Drop for StartToken {
fn drop(&mut self) {
if self.0.in_flight.fetch_sub(1, Ordering::AcqRel) == 1 {
self.0.none_left.notify_waiters();
}
}
}
impl Ctx {
pub(crate) fn new(
executor: Arc<Executor>,
workflow_id: impl Into<String>,
owner_xid: impl Into<String>,
deadline: Option<Timestamp>,
encoding: PayloadEncoding,
) -> Self {
Self {
executor,
workflow: Arc::new(WorkflowState {
workflow_id: workflow_id.into(),
owner_xid: owner_xid.into(),
deadline,
encoding,
next_step_id: AtomicI32::new(0),
starts: Arc::default(),
}),
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 owner_xid(&self) -> &str {
&self.workflow.owner_xid
}
pub(crate) fn owner(&self) -> crate::sysdb::types::WorkflowOwner<'_> {
crate::sysdb::types::WorkflowOwner {
workflow_id: self.workflow_id(),
owner_xid: self.owner_xid(),
}
}
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 starts(&self) -> &Arc<Starts> {
&self.workflow.starts
}
pub(crate) fn deadline(&self) -> Option<Timestamp> {
self.workflow.deadline
}
pub(crate) fn encoding(&self) -> PayloadEncoding {
self.workflow.encoding.clone()
}
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 peek_step_id(&self) -> i32 {
self.workflow.peek_step_id()
}
pub(crate) fn claim_step_id(&self, step_id: i32) -> bool {
self.workflow.claim_step_id(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)]
pub(crate) const TEST_OWNER: &str = "test-owner";
#[cfg(test)]
pub(crate) async fn own_for_test(db: &dbos_test_support::TestDatabase, workflow_id: &str) {
sqlx::query(r#"UPDATE "dbos"."workflow_status" SET owner_xid = $1 WHERE workflow_uuid = $2"#)
.bind(TEST_OWNER)
.bind(workflow_id)
.execute(&db.pool().await)
.await
.expect("could not take ownership of the workflow row");
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn state() -> WorkflowState {
WorkflowState {
workflow_id: "wf-1".to_owned(),
owner_xid: TEST_OWNER.to_owned(),
deadline: None,
encoding: PayloadEncoding::Native,
next_step_id: AtomicI32::new(0),
starts: Arc::default(),
}
}
#[tokio::test]
async fn no_starts_in_flight_drains_at_once() {
let starts = Arc::new(Starts::default());
crate::rt::timeout(Duration::from_secs(1), starts.drained())
.await
.expect("nothing was in flight");
}
#[tokio::test]
async fn drained_waits_for_the_last_start() {
let starts = Arc::new(Starts::default());
let first = starts.token();
let second = starts.token();
let mut drained = std::pin::pin!(starts.drained());
assert!(
crate::rt::timeout(Duration::from_millis(20), drained.as_mut())
.await
.is_err(),
"drained with two starts in flight"
);
drop(first);
assert!(
crate::rt::timeout(Duration::from_millis(20), drained.as_mut())
.await
.is_err(),
"drained with one start still in flight"
);
drop(second);
crate::rt::timeout(Duration::from_secs(1), drained)
.await
.expect("the last start was dropped");
}
#[tokio::test]
async fn a_token_dropped_elsewhere_wakes_the_wait() {
let starts = Arc::new(Starts::default());
let token = starts.token();
let dropper = crate::rt::spawn(async move {
crate::rt::sleep(Duration::from_millis(20)).await;
drop(token);
});
crate::rt::timeout(Duration::from_secs(1), starts.drained())
.await
.expect("the token was dropped");
dropper.await.unwrap();
}
#[test]
fn step_ids_are_zero_based() {
let state = state();
assert_eq!(state.next_step_id(), 0, "step ids are zero-based");
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,
crate::context::TEST_OWNER,
deadline,
PayloadEncoding::Native,
);
(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");
crate::rt::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 {
crate::rt::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;
}
}