Files
ruin/lib/runtime/src/time.rs

240 lines
6.7 KiB
Rust

//! Runtime time primitives.
//!
//! These helpers integrate with the runtime's timer queue and are designed to be used from
//! futures scheduled with [`crate::queue_future`] or one of the runtime entry macros.
use alloc::rc::Rc;
use core::cell::{Cell, RefCell};
use core::fmt;
use core::future::{Future, poll_fn};
use core::pin::Pin;
use core::task::Waker;
use core::task::{Context, Poll};
use core::time::Duration;
use crate::{clear_timeout, set_timeout};
/// Future returned by [`sleep`].
pub struct Sleep {
delay: Option<Duration>,
state: Option<Rc<SleepState>>,
handle: Option<crate::TimeoutHandle>,
completed: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Error returned by [`timeout`] when the deadline expires first.
pub struct Elapsed;
/// Returns a future that completes after `duration` has elapsed on the current runtime thread.
///
/// # Examples
///
/// ```
/// # let _ = || async {
/// ruin_runtime::time::sleep(std::time::Duration::from_millis(10)).await;
/// # };
/// ```
pub fn sleep(duration: Duration) -> Sleep {
Sleep {
delay: Some(duration),
state: None,
handle: None,
completed: false,
}
}
/// Runs `future` until it completes or `duration` elapses, whichever happens first.
///
/// The wrapped future is dropped when the timeout fires. As with other runtime operations, dropping
/// a future cancels interest in the result but does not guarantee cancellation of any underlying
/// OS work that future may have started.
///
/// # Examples
///
/// ```
/// # let _ = || async {
/// let result = ruin_runtime::time::timeout(
/// std::time::Duration::from_millis(5),
/// async { 42usize },
/// )
/// .await;
/// assert_eq!(result, Ok(42));
/// # };
/// ```
pub async fn timeout<F>(duration: Duration, future: F) -> Result<F::Output, Elapsed>
where
F: Future,
{
let mut future = std::pin::pin!(future);
let mut sleeper = std::pin::pin!(sleep(duration));
poll_fn(|cx| {
if let Poll::Ready(output) = future.as_mut().poll(cx) {
return Poll::Ready(Ok(output));
}
if let Poll::Ready(()) = sleeper.as_mut().poll(cx) {
return Poll::Ready(Err(Elapsed));
}
Poll::Pending
})
.await
}
impl Future for Sleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.completed {
return Poll::Ready(());
}
if self.state.is_none() {
let delay = self.delay.take().unwrap_or(Duration::ZERO);
let state = Rc::new(SleepState::default());
let state_for_callback = Rc::clone(&state);
let timeout_handle = set_timeout(delay, move || state_for_callback.complete());
self.state = Some(state);
self.handle = Some(timeout_handle);
}
let state = self
.state
.as_ref()
.expect("sleep state should be initialized");
if state.ready.get() {
self.completed = true;
self.state = None;
self.handle = None;
Poll::Ready(())
} else {
*state.waker.borrow_mut() = Some(cx.waker().clone());
if state.ready.get() {
self.completed = true;
self.state = None;
self.handle = None;
Poll::Ready(())
} else {
Poll::Pending
}
}
}
}
impl Drop for Sleep {
fn drop(&mut self) {
if self.completed {
return;
}
if let Some(handle) = self.handle.take() {
clear_timeout(&handle);
}
}
}
#[derive(Default)]
struct SleepState {
ready: Cell<bool>,
waker: RefCell<Option<Waker>>,
}
impl SleepState {
fn complete(&self) {
self.ready.set(true);
if let Some(waker) = self.waker.borrow_mut().take() {
waker.wake();
}
}
}
impl fmt::Display for Elapsed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("deadline elapsed")
}
}
impl std::error::Error for Elapsed {}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::{queue_future, queue_task, run};
use super::{sleep, timeout};
#[test]
fn sleep_and_timeout_work() {
let log = std::thread::spawn(|| {
let log = Arc::new(Mutex::new(Vec::new()));
let log_for_task = Arc::clone(&log);
queue_task(move || {
let log_for_task = Arc::clone(&log_for_task);
queue_future(async move {
log_for_task.lock().unwrap().push("started");
sleep(Duration::from_millis(5)).await;
log_for_task.lock().unwrap().push("slept");
let result = timeout(Duration::from_millis(5), async {
sleep(Duration::from_millis(20)).await;
42usize
})
.await;
assert!(result.is_err(), "timeout should fire first");
log_for_task.lock().unwrap().push("timed out");
});
});
run();
let log = log.lock().unwrap();
log.clone()
})
.join()
.expect("time test thread should join successfully");
assert_eq!(log.as_slice(), ["started", "slept", "timed out"]);
}
/// Verify that `sleep(Duration::ZERO).await` yields to the macrotask queue
/// before the future continues. A macrotask queued before the sleep must
/// run before the future's continuation.
#[test]
fn sleep_zero_yields_to_macrotask_queue() {
let order = std::thread::spawn(|| {
let order = Rc::new(RefCell::new(Vec::<&'static str>::new()));
// Macrotask queued before the sleep.
{
let order = Rc::clone(&order);
queue_task(move || order.borrow_mut().push("macrotask"));
}
// Future that awaits sleep(ZERO) and then records its continuation.
{
let order = Rc::clone(&order);
queue_future(async move {
sleep(Duration::ZERO).await;
order.borrow_mut().push("after_sleep");
});
}
run();
Rc::try_unwrap(order).unwrap().into_inner()
})
.join()
.expect("test thread should join");
// The macrotask must run before the sleep future continues, because
// sleep(ZERO) resolves via a timer event (macrotask), so the queued
// macrotask runs first.
assert_eq!(order.as_slice(), ["macrotask", "after_sleep"]);
}
}