mod task;
#[cfg(feature = "runtime-async-global-executor")]
mod with_async_global_executor;
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
mod with_async_io;
#[cfg(all(
feature = "runtime-smol",
not(feature = "runtime-async-global-executor")
))]
mod with_smol;
#[cfg(feature = "runtime-tokio")]
mod with_tokio;
#[cfg(feature = "runtime-async-global-executor")]
use with_async_global_executor as global;
#[cfg(all(
feature = "runtime-smol",
not(feature = "runtime-async-global-executor")
))]
use with_smol as global;
use std::future::Future;
use std::task::Poll;
use std::time::{Duration, Instant};
pub use task::{AbortHandle, JoinError, JoinHandle};
#[cfg(not(any(
feature = "runtime-tokio",
feature = "runtime-smol",
feature = "runtime-async-global-executor"
)))]
compile_error!(
"dbos needs an async runtime: enable one of the features `runtime-tokio` (the default; check \
for `default-features = false`), `runtime-smol` or `runtime-async-global-executor`"
);
#[derive(Clone)]
pub(crate) struct Spawner(Backend);
#[derive(Clone)]
enum Backend {
#[cfg(feature = "runtime-tokio")]
#[allow(clippy::disallowed_types)]
Tokio(tokio::runtime::Handle),
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
Global,
}
impl Spawner {
#[track_caller]
pub(crate) fn current() -> Self {
#[cfg(feature = "runtime-tokio")]
if let Some(handle) = with_tokio::current() {
return Self(Backend::Tokio(handle));
}
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
return Self(Backend::Global);
#[cfg(not(any(feature = "runtime-smol", feature = "runtime-async-global-executor")))]
missing_runtime()
}
pub(crate) fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let (task, handle) = task::joinable(future);
match self.0 {
#[cfg(feature = "runtime-tokio")]
Backend::Tokio(ref runtime) => with_tokio::spawn_detached(runtime, task),
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
Backend::Global => global::spawn_detached(task),
}
handle
}
}
#[track_caller]
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
Spawner::current().spawn(future)
}
pub fn sleep(duration: Duration) -> impl Future<Output = ()> + Send {
sleep_until(after(duration))
}
pub async fn sleep_until(deadline: Instant) {
#[cfg(feature = "runtime-tokio")]
if with_tokio::available() {
return with_tokio::sleep_until(deadline).await;
}
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
return with_async_io::sleep_until(deadline).await;
#[cfg(not(any(feature = "runtime-smol", feature = "runtime-async-global-executor")))]
missing_runtime()
}
pub fn timeout<F: Future>(
duration: Duration,
future: F,
) -> impl Future<Output = Result<F::Output, Elapsed>> {
timeout_at(after(duration), future)
}
pub async fn timeout_at<F: Future>(deadline: Instant, future: F) -> Result<F::Output, Elapsed> {
#[cfg(feature = "runtime-tokio")]
if with_tokio::available() {
return with_tokio::timeout_at(deadline, future).await;
}
let mut future = std::pin::pin!(future);
let mut expiry = std::pin::pin!(sleep_until(deadline));
std::future::poll_fn(|cx| {
if let Poll::Ready(output) = future.as_mut().poll(cx) {
return Poll::Ready(Ok(output));
}
expiry.as_mut().poll(cx).map(|()| Err(Elapsed))
})
.await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Elapsed;
impl std::fmt::Display for Elapsed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("deadline has elapsed")
}
}
impl std::error::Error for Elapsed {}
#[cfg(test)]
pub(crate) async fn yield_now() {
let mut yielded = false;
std::future::poll_fn(|cx| {
if yielded {
Poll::Ready(())
} else {
yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
})
.await;
}
pub fn fallback() -> Option<&'static str> {
if cfg!(feature = "runtime-async-global-executor") {
Some("async-global-executor")
} else if cfg!(feature = "runtime-smol") {
Some("smol")
} else {
None
}
}
pub(crate) fn now() -> Instant {
#[cfg(feature = "runtime-tokio")]
if with_tokio::available() {
return with_tokio::now();
}
Instant::now()
}
fn after(duration: Duration) -> Instant {
let now = now();
now.checked_add(duration)
.unwrap_or_else(|| now + Duration::from_secs(86_400 * 365 * 30))
}
#[cfg(not(any(feature = "runtime-smol", feature = "runtime-async-global-executor")))]
#[track_caller]
fn missing_runtime() -> ! {
panic!(
"dbos needs an async runtime, and none of the enabled ones is available here: call it \
from within a tokio runtime, or enable `runtime-smol` or \
`runtime-async-global-executor`"
)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
macro_rules! on_each_runtime {
($($scenario:ident),* $(,)?) => {
mod on_tokio {
$(
#[tokio::test]
async fn $scenario() {
super::$scenario().await;
}
)*
}
#[cfg(any(feature = "runtime-smol", feature = "runtime-async-global-executor"))]
mod off_tokio {
$(
#[test]
fn $scenario() {
async_io::block_on(super::$scenario());
}
)*
}
};
}
on_each_runtime!(
joining_gives_the_output,
dropping_the_handle_detaches_the_task,
an_aborted_task_is_dropped_and_reports_finished,
a_panic_reaches_the_joiner,
a_panic_dropping_an_aborted_body_reaches_the_joiner,
a_ready_future_beats_a_passed_deadline,
a_sleep_waits_out_its_duration,
an_unbounded_timeout_does_not_overflow,
);
async fn joining_gives_the_output() {
assert_eq!(super::spawn(async { 7 }).await.expect("it completes"), 7);
}
async fn dropping_the_handle_detaches_the_task() {
let (done, finished) = tokio::sync::oneshot::channel();
drop(super::spawn(async move {
super::yield_now().await;
let _ = done.send(());
}));
super::timeout(Duration::from_secs(10), finished)
.await
.expect("the task ran on without its handle")
.expect("and finished");
}
async fn an_aborted_task_is_dropped_and_reports_finished() {
let (dropped, gone) = tokio::sync::oneshot::channel::<()>();
let task = super::spawn(async move {
let _dropped = dropped;
std::future::pending::<()>().await;
});
let abort = task.abort_handle();
assert!(!abort.is_finished());
abort.abort();
assert!(task.await.expect_err("it was aborted").is_cancelled());
assert!(abort.is_finished());
assert!(
gone.await.is_err(),
"the body was dropped, and its sender with it"
);
}
async fn a_panic_reaches_the_joiner() {
let joined = super::spawn(async { panic!("boom") }).await;
let payload = joined.expect_err("it panicked").into_panic();
assert_eq!(payload.downcast_ref::<&str>(), Some(&"boom"));
}
struct PanicsOnDrop;
impl Drop for PanicsOnDrop {
fn drop(&mut self) {
panic!("in drop");
}
}
async fn a_panic_dropping_an_aborted_body_reaches_the_joiner() {
let (started, running) = tokio::sync::oneshot::channel();
let task = super::spawn(async move {
let _guard = PanicsOnDrop;
let _ = started.send(());
std::future::pending::<()>().await;
});
running.await.expect("the body started");
task.abort();
let payload = task.await.expect_err("it was aborted").into_panic();
assert_eq!(payload.downcast_ref::<&str>(), Some(&"in drop"));
}
async fn a_ready_future_beats_a_passed_deadline() {
let past = Instant::now() - Duration::from_secs(1);
assert_eq!(super::timeout_at(past, async { 1 }).await, Ok(1));
assert_eq!(
super::timeout_at(past, std::future::pending::<()>()).await,
Err(super::Elapsed)
);
}
async fn a_sleep_waits_out_its_duration() {
let started = Instant::now();
super::sleep(Duration::from_millis(20)).await;
assert!(started.elapsed() >= Duration::from_millis(20));
}
async fn an_unbounded_timeout_does_not_overflow() {
assert_eq!(super::timeout(Duration::MAX, async { 1 }).await, Ok(1));
}
#[test]
fn a_future_that_spends_tokios_budget_still_times_out() {
let (done, finished) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a runtime");
let elapsed = runtime.block_on(async {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let busy = async move {
while tx.send(()).is_ok() {
rx.recv().await;
}
};
super::timeout(Duration::from_millis(20), busy).await
});
let _ = done.send(elapsed);
});
let elapsed = finished
.recv_timeout(Duration::from_secs(10))
.expect("the timeout never fired");
assert_eq!(elapsed, Err(super::Elapsed));
}
#[tokio::test(start_paused = true)]
async fn deadlines_follow_tokios_clock() {
tokio::time::advance(Duration::from_secs(3600)).await;
let started = super::now();
super::sleep(Duration::from_secs(60)).await;
assert!(super::now() - started >= Duration::from_secs(60));
let timed_out = super::now();
assert_eq!(
super::timeout(Duration::from_secs(60), std::future::pending::<()>()).await,
Err(super::Elapsed)
);
assert!(super::now() - timed_out >= Duration::from_secs(60));
}
#[test]
fn a_task_its_runtime_drops_reports_finished_and_cancelled() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a runtime");
let ran = Arc::new(AtomicBool::new(false));
let (polled, unpolled) = runtime.block_on(async {
let (started, running) = tokio::sync::oneshot::channel();
let polled = super::spawn(async move {
let _ = started.send(());
std::future::pending::<()>().await;
});
running.await.expect("the first task started");
let ran = Arc::clone(&ran);
let unpolled = super::spawn(async move { ran.store(true, Ordering::SeqCst) });
(polled, unpolled)
});
assert!(!polled.is_finished() && !unpolled.is_finished());
drop(runtime);
assert!(
!ran.load(Ordering::SeqCst),
"the second task was never polled"
);
for task in [polled, unpolled] {
assert!(
task.is_finished(),
"a dropped task is finished, as tokio counts it"
);
let joined = tokio::runtime::Builder::new_current_thread()
.build()
.expect("a runtime")
.block_on(task);
assert!(joined.expect_err("it never completed").is_cancelled());
}
}
#[test]
#[allow(clippy::async_yields_async)]
fn a_panic_dropping_a_body_its_runtime_drops_reaches_the_joiner() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a runtime");
let task = runtime.block_on(async {
let (started, running) = tokio::sync::oneshot::channel();
let task = super::spawn(async move {
let _guard = PanicsOnDrop;
let _ = started.send(());
std::future::pending::<()>().await;
});
running.await.expect("the task started");
task
});
drop(runtime);
assert!(task.is_finished());
let joined = tokio::runtime::Builder::new_current_thread()
.build()
.expect("a runtime")
.block_on(task);
let payload = joined.expect_err("it never completed").into_panic();
assert_eq!(payload.downcast_ref::<&str>(), Some(&"in drop"));
}
#[tokio::test]
async fn a_task_aborted_before_it_starts_is_never_polled() {
let polled = Arc::new(AtomicBool::new(false));
let task = {
let polled = Arc::clone(&polled);
super::Spawner::current().spawn(async move { polled.store(true, Ordering::SeqCst) })
};
task.abort();
let joined = task.await;
assert!(joined.expect_err("it was aborted").is_cancelled());
assert!(!polled.load(Ordering::SeqCst));
}
}