use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use async_trait::async_trait;
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait Runtime: Send + Sync + 'static {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> AbortHandle;
fn spawn_detached(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
self.spawn(future).detach();
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>>;
fn spawn_blocking(
&self,
f: Box<dyn FnOnce() + Send + 'static>,
) -> Pin<Box<dyn Future<Output = ()> + Send>>;
fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>>;
fn yield_frequency(&self) -> u32 {
10
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait Runtime: Send + Sync + 'static {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) -> AbortHandle;
fn spawn_detached(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) {
self.spawn(future).detach();
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()>>>;
fn spawn_blocking(&self, f: Box<dyn FnOnce() + 'static>) -> Pin<Box<dyn Future<Output = ()>>>;
fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()>>>>;
fn yield_frequency(&self) -> u32 {
10
}
}
#[cfg(not(target_arch = "wasm32"))]
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[cfg(not(target_arch = "wasm32"))]
pub trait Spawnable: Send + 'static {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send + 'static> Spawnable for T {}
#[cfg(target_arch = "wasm32")]
pub trait Spawnable: 'static {}
#[cfg(target_arch = "wasm32")]
impl<T: 'static> Spawnable for T {}
#[must_use = "dropping an AbortHandle aborts the task; use .detach() for fire-and-forget"]
pub struct AbortHandle {
abort_fn: std::sync::Mutex<Option<Box<dyn FnOnce() + Send + 'static>>>,
}
impl AbortHandle {
pub fn new(abort_fn: impl FnOnce() + Send + 'static) -> Self {
Self {
abort_fn: std::sync::Mutex::new(Some(Box::new(abort_fn))),
}
}
pub fn noop() -> Self {
Self {
abort_fn: std::sync::Mutex::new(None),
}
}
pub fn abort(&self) {
if let Some(f) = self
.abort_fn
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
f();
}
}
pub fn detach(self) {
*self.abort_fn.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
}
impl Drop for AbortHandle {
fn drop(&mut self) {
self.abort();
}
}
pub struct ShutdownNotifier {
inner: std::sync::Arc<ShutdownInner>,
}
struct ShutdownInner {
fired: std::sync::atomic::AtomicBool,
event: event_listener::Event,
}
impl ShutdownNotifier {
pub fn new() -> Self {
Self {
inner: std::sync::Arc::new(ShutdownInner {
fired: std::sync::atomic::AtomicBool::new(false),
event: event_listener::Event::new(),
}),
}
}
pub fn notify(&self) {
self.inner
.fired
.store(true, std::sync::atomic::Ordering::SeqCst);
self.inner.event.notify(usize::MAX);
}
fn is_fired(&self) -> bool {
self.inner.fired.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn listen(&self) -> impl Future<Output = ()> + use<> {
let listener = self.inner.event.listen();
let fired = self.is_fired();
async move {
if fired {
return;
}
listener.await;
}
}
pub fn subscribe(&self) -> ShutdownSignal {
ShutdownSignal {
inner: Some(std::sync::Arc::clone(&self.inner)),
}
}
}
impl Default for ShutdownNotifier {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct ShutdownSignal {
inner: Option<std::sync::Arc<ShutdownInner>>,
}
impl ShutdownSignal {
pub fn never() -> Self {
Self { inner: None }
}
pub fn is_fired(&self) -> bool {
self.inner
.as_ref()
.is_some_and(|i| i.fired.load(std::sync::atomic::Ordering::SeqCst))
}
}
pub fn wait_for_shutdown(signal: &ShutdownSignal) -> impl Future<Output = ()> + use<> {
let (fired, listener) = match signal.inner.as_ref() {
Some(inner) => {
let listener = inner.event.listen();
let fired = inner.fired.load(std::sync::atomic::Ordering::SeqCst);
(fired, Some(listener))
}
None => (false, None),
};
async move {
if fired {
return;
}
match listener {
Some(l) => l.await,
None => std::future::pending::<()>().await,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("operation timed out")]
pub struct Elapsed;
pub async fn timeout<F, T>(rt: &dyn Runtime, duration: Duration, future: F) -> Result<T, Elapsed>
where
F: Future<Output = T>,
{
use futures::future::Either;
futures::pin_mut!(future);
let sleep = rt.sleep(duration);
futures::pin_mut!(sleep);
match futures::future::select(future, sleep).await {
Either::Left((result, _)) => Ok(result),
Either::Right(((), _)) => Err(Elapsed),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn blocking<T: Send + 'static>(
rt: &dyn Runtime,
f: impl FnOnce() -> T + Send + 'static,
) -> T {
let (tx, rx) = futures::channel::oneshot::channel();
rt.spawn_blocking(Box::new(move || {
let _ = tx.send(f());
}))
.await;
rx.await.unwrap_or_else(|_| {
panic!("blocking task failed to complete (closure panic or runtime shutdown)")
})
}
#[cfg(target_arch = "wasm32")]
pub async fn blocking<T: 'static>(_rt: &dyn Runtime, f: impl FnOnce() -> T + 'static) -> T {
f()
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod abort_handle_tests {
use super::AbortHandle;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn counting_handle() -> (AbortHandle, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
let handle = {
let calls = Arc::clone(&calls);
AbortHandle::new(move || {
calls.fetch_add(1, Ordering::SeqCst);
})
};
(handle, calls)
}
#[test]
fn abort_runs_the_callback_exactly_once() {
let (handle, calls) = counting_handle();
handle.abort();
assert_eq!(calls.load(Ordering::SeqCst), 1);
handle.abort();
drop(handle);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn drop_aborts_an_unaborted_handle() {
let (handle, calls) = counting_handle();
assert_eq!(calls.load(Ordering::SeqCst), 0);
drop(handle);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn detach_disarms_the_abort_on_drop() {
let (handle, calls) = counting_handle();
handle.detach();
assert_eq!(
calls.load(Ordering::SeqCst),
0,
"a detached handle must leave the task running"
);
}
#[test]
fn noop_handle_is_inert() {
let handle = AbortHandle::noop();
handle.abort();
drop(handle);
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tokio_runtime_tests {
use super::{AbortHandle, Elapsed, Runtime, timeout};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
struct TokioTestRuntime;
#[async_trait::async_trait]
impl Runtime for TokioTestRuntime {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> AbortHandle {
let handle = tokio::spawn(future);
AbortHandle::new(move || handle.abort())
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(tokio::time::sleep(duration))
}
fn spawn_blocking(
&self,
f: Box<dyn FnOnce() + Send + 'static>,
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async move {
let _ = tokio::task::spawn_blocking(f).await;
})
}
fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>> {
None
}
}
struct DropSignal(Option<futures::channel::oneshot::Sender<()>>);
impl Drop for DropSignal {
fn drop(&mut self) {
if let Some(tx) = self.0.take() {
let _ = tx.send(());
}
}
}
async fn spawn_parked_task() -> (AbortHandle, futures::channel::oneshot::Receiver<()>) {
let (tx, rx) = futures::channel::oneshot::channel();
let (started_tx, started_rx) = futures::channel::oneshot::channel();
let dropped_on_cancel = DropSignal(Some(tx));
let handle = TokioTestRuntime.spawn(Box::pin(async move {
let _dropped_on_cancel = dropped_on_cancel;
let _ = started_tx.send(());
std::future::pending::<()>().await;
}));
tokio::time::timeout(TEST_TIMEOUT, started_rx)
.await
.expect("timed out waiting for the spawned task to start")
.expect("the spawned task was cancelled before it started");
(handle, rx)
}
async fn expect_cancelled(rx: futures::channel::oneshot::Receiver<()>) {
tokio::time::timeout(TEST_TIMEOUT, rx)
.await
.expect("timed out waiting for the spawned task to be cancelled")
.expect("the task's drop signal was lost");
}
#[tokio::test]
async fn abort_cancels_a_spawned_task() {
let (handle, rx) = spawn_parked_task().await;
handle.abort();
expect_cancelled(rx).await;
}
#[tokio::test]
async fn dropping_the_handle_cancels_a_spawned_task() {
let (handle, rx) = spawn_parked_task().await;
drop(handle);
expect_cancelled(rx).await;
}
#[tokio::test]
async fn detached_task_survives_its_handle() {
let (tx, rx) = futures::channel::oneshot::channel();
TokioTestRuntime
.spawn(Box::pin(async move {
let _ = tx.send(7u32);
}))
.detach();
let value = tokio::time::timeout(TEST_TIMEOUT, rx)
.await
.expect("timed out waiting for the detached task")
.expect("the detached task was cancelled");
assert_eq!(value, 7);
}
#[tokio::test]
async fn timeout_returns_the_value_when_the_future_wins() {
let result = timeout(&TokioTestRuntime, Duration::from_secs(30), async { 42u32 }).await;
assert_eq!(result, Ok(42));
}
#[tokio::test]
async fn timeout_elapses_when_the_future_never_completes() {
let result = timeout(
&TokioTestRuntime,
Duration::from_millis(5),
std::future::pending::<()>(),
)
.await;
assert_eq!(result, Err(Elapsed));
assert_eq!(Elapsed.to_string(), "operation timed out");
}
#[tokio::test]
async fn timeout_does_not_wait_out_the_duration_on_success() {
tokio::time::timeout(TEST_TIMEOUT, async {
let result = timeout(&TokioTestRuntime, Duration::from_secs(60), async { "ok" }).await;
assert_eq!(result, Ok("ok"));
})
.await
.expect("timeout() must return as soon as the inner future completes");
}
#[tokio::test]
async fn blocking_ferries_the_closure_result_back() {
let value =
tokio::time::timeout(TEST_TIMEOUT, super::blocking(&TokioTestRuntime, || 6 * 7))
.await
.expect("timed out waiting for the blocking closure");
assert_eq!(value, 42);
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod shutdown_tests {
use super::{ShutdownNotifier, ShutdownSignal, wait_for_shutdown};
use futures::FutureExt;
use futures::executor::block_on;
#[test]
fn wait_for_shutdown_catches_notify_fired_before_subscribe() {
let notifier = ShutdownNotifier::new();
notifier.notify();
let signal = notifier.subscribe();
block_on(wait_for_shutdown(&signal));
}
#[test]
fn notifier_listen_catches_notify_fired_before_listen() {
let notifier = ShutdownNotifier::new();
notifier.notify();
block_on(notifier.listen());
}
#[test]
fn wait_for_shutdown_wakes_on_notify_after_subscribe() {
let notifier = ShutdownNotifier::new();
let signal = notifier.subscribe();
let fut = wait_for_shutdown(&signal);
notifier.notify();
block_on(fut);
}
#[test]
fn notify_wakes_every_registered_listener() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingWaker(AtomicUsize);
impl futures::task::ArcWake for CountingWaker {
fn wake_by_ref(arc_self: &Arc<Self>) {
arc_self.0.fetch_add(1, Ordering::Relaxed);
}
}
let notifier = ShutdownNotifier::new();
let signals: Vec<_> = (0..4).map(|_| notifier.subscribe()).collect();
let mut waiters: Vec<_> = signals
.iter()
.map(|s| Box::pin(wait_for_shutdown(s).fuse()))
.collect();
let mut publisher_side = Box::pin(notifier.listen().fuse());
let wakers: Vec<_> = (0..signals.len() + 1)
.map(|_| Arc::new(CountingWaker(AtomicUsize::new(0))))
.collect();
for (fut, waker) in waiters.iter_mut().zip(&wakers) {
let waker = futures::task::waker_ref(waker);
let mut ctx = futures::task::Context::from_waker(&waker);
assert!(fut.as_mut().poll_unpin(&mut ctx).is_pending());
}
{
let waker = futures::task::waker_ref(wakers.last().expect("wakers is non-empty"));
let mut ctx = futures::task::Context::from_waker(&waker);
assert!(publisher_side.as_mut().poll_unpin(&mut ctx).is_pending());
}
assert!(
wakers.iter().all(|w| w.0.load(Ordering::Relaxed) == 0),
"no listener may be woken before notify()"
);
notifier.notify();
for (i, waker) in wakers.iter().enumerate() {
assert!(
waker.0.load(Ordering::Relaxed) > 0,
"listener {i} of {} was never woken by notify()",
wakers.len()
);
}
block_on(futures::future::join_all(waiters));
block_on(publisher_side);
assert!(signals.iter().all(|s| s.is_fired()));
}
#[test]
fn notify_is_idempotent_for_late_subscribers() {
let notifier = ShutdownNotifier::new();
let early = notifier.subscribe();
notifier.notify();
notifier.notify();
let late = notifier.subscribe();
assert!(early.is_fired());
assert!(late.is_fired());
block_on(futures::future::join(
wait_for_shutdown(&early),
wait_for_shutdown(&late),
));
}
#[test]
fn cloned_signals_observe_the_same_notify() {
let notifier = ShutdownNotifier::new();
let signal = notifier.subscribe();
let clone = signal.clone();
assert!(!signal.is_fired());
assert!(!clone.is_fired());
notifier.notify();
assert!(signal.is_fired());
assert!(clone.is_fired());
block_on(wait_for_shutdown(&clone));
}
#[test]
fn unfired_notifier_leaves_listeners_pending() {
let notifier = ShutdownNotifier::default();
let signal = notifier.subscribe();
assert!(!signal.is_fired());
let mut fut = Box::pin(wait_for_shutdown(&signal).fuse());
let mut ctx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
assert!(fut.as_mut().poll_unpin(&mut ctx).is_pending());
}
#[test]
fn wait_for_shutdown_never_stays_pending() {
let signal = ShutdownSignal::never();
let mut fut = Box::pin(wait_for_shutdown(&signal).fuse());
let mut ctx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
assert!(fut.as_mut().poll_unpin(&mut ctx).is_pending());
}
#[test]
fn captured_signal_observes_fire_after_notifier_dropped() {
let notifier = ShutdownNotifier::new();
let signal = notifier.subscribe();
notifier.notify();
drop(notifier);
assert!(
signal.is_fired(),
"Signal must remain fired after the publisher was dropped"
);
block_on(wait_for_shutdown(&signal));
}
}