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 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 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
}
}
#[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 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 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));
}
}