use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
use core::time::Duration;
use futures::stream::Stream;
use pin_project_lite::pin_project;
pub mod drivers;
mod missed_tick;
mod types;
pub use missed_tick::MissedTickBehavior;
pub use types::{Driver, Instant};
include!(concat!(env!("OUT_DIR"), "/proxima_time_bound_driver.rs"));
#[must_use]
pub fn now() -> Instant {
BOUND_DRIVER.now()
}
#[derive(Debug, thiserror::Error)]
#[error("deadline has elapsed")]
pub struct Elapsed(());
pub struct Sleep {
deadline: Instant,
}
impl Sleep {
#[must_use]
pub fn until(deadline: Instant) -> Self {
Self { deadline }
}
#[must_use]
pub fn deadline(&self) -> Instant {
self.deadline
}
}
impl Future for Sleep {
type Output = ();
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
if BOUND_DRIVER.now() >= self.deadline {
Poll::Ready(())
} else {
BOUND_DRIVER.schedule_wake(self.deadline, context.waker().clone());
Poll::Pending
}
}
}
#[must_use]
pub fn sleep(duration: Duration) -> Sleep {
Sleep::until(now() + duration)
}
#[must_use]
pub fn sleep_until(deadline: Instant) -> Sleep {
Sleep::until(deadline)
}
pin_project! {
struct Timeout<Fut> {
#[pin]
future: Fut,
deadline: Instant,
}
}
impl<Fut: Future> Future for Timeout<Fut> {
type Output = Result<Fut::Output, Elapsed>;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if let Poll::Ready(value) = this.future.poll(context) {
return Poll::Ready(Ok(value));
}
if BOUND_DRIVER.now() >= *this.deadline {
return Poll::Ready(Err(Elapsed(())));
}
BOUND_DRIVER.schedule_wake(*this.deadline, context.waker().clone());
Poll::Pending
}
}
pub fn timeout<Fut>(
duration: Duration,
future: Fut,
) -> impl Future<Output = Result<Fut::Output, Elapsed>>
where
Fut: Future,
{
Timeout {
future,
deadline: now() + duration,
}
}
pub fn timeout_at<Fut>(
deadline: Instant,
future: Fut,
) -> impl Future<Output = Result<Fut::Output, Elapsed>>
where
Fut: Future,
{
Timeout { future, deadline }
}
#[must_use]
pub fn interval(period: Duration) -> Interval {
Interval {
period,
next_deadline: None,
missed_tick_behavior: MissedTickBehavior::default(),
}
}
#[must_use]
pub fn interval_at(start: Instant, period: Duration) -> Interval {
Interval {
period,
next_deadline: Some(start),
missed_tick_behavior: MissedTickBehavior::default(),
}
}
pub struct Interval {
period: Duration,
next_deadline: Option<Instant>,
missed_tick_behavior: MissedTickBehavior,
}
impl Interval {
pub fn set_missed_tick_behavior(&mut self, behavior: MissedTickBehavior) {
self.missed_tick_behavior = behavior;
}
#[must_use]
pub fn missed_tick_behavior(&self) -> MissedTickBehavior {
self.missed_tick_behavior
}
#[must_use]
pub fn period(&self) -> Duration {
self.period
}
pub fn tick(&mut self) -> Tick<'_> {
Tick { interval: self }
}
}
pub struct Tick<'a> {
interval: &'a mut Interval,
}
impl Future for Tick<'_> {
type Output = Instant;
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Instant> {
let now = now();
let interval = &mut *self.interval;
if interval.next_deadline.is_none() {
interval.next_deadline = Some(now + interval.period);
return Poll::Ready(now);
}
let Some(deadline) = interval.next_deadline else {
return Poll::Pending;
};
if now < deadline {
BOUND_DRIVER.schedule_wake(deadline, context.waker().clone());
return Poll::Pending;
}
let fired_at = deadline;
let next_deadline = match interval.missed_tick_behavior {
MissedTickBehavior::Burst => fired_at + interval.period,
MissedTickBehavior::Delay => now + interval.period,
MissedTickBehavior::Skip => {
let elapsed_periods = now
.saturating_duration_since(fired_at)
.as_nanos()
.checked_div(interval.period.as_nanos().max(1))
.unwrap_or(0);
let skip = u32::try_from(elapsed_periods).unwrap_or(u32::MAX);
fired_at + interval.period.saturating_mul(skip.saturating_add(1))
}
};
interval.next_deadline = Some(next_deadline);
Poll::Ready(fired_at)
}
}
impl Stream for Interval {
type Item = ();
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<()>> {
let interval: &mut Interval = &mut self;
let mut tick = Tick { interval };
match Pin::new(&mut tick).poll(context) {
Poll::Ready(_instant) => Poll::Ready(Some(())),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(all(test, feature = "time-driver-std-thread"))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use futures::executor::block_on;
use futures::stream::StreamExt;
#[test]
fn sleep_resolves_after_duration() {
let start = now();
block_on(sleep(Duration::from_millis(30)));
assert!(now().saturating_duration_since(start) >= Duration::from_millis(25));
}
#[test]
fn timeout_returns_ok_when_future_completes_in_time() {
let outcome = block_on(timeout(Duration::from_millis(100), async { 7_u32 }));
assert_eq!(outcome.expect("ok"), 7);
}
#[test]
fn timeout_returns_elapsed_when_timer_fires_first() {
let outcome: Result<u32, Elapsed> = block_on(timeout(Duration::from_millis(10), async {
sleep(Duration::from_secs(1)).await;
7_u32
}));
assert!(outcome.is_err());
}
#[test]
fn interval_emits_first_tick_immediately_then_periodic() {
block_on(async {
let mut interval = interval(Duration::from_millis(20));
let start = now();
for _ in 0..3_u32 {
interval.next().await.expect("tick");
}
assert!(now().saturating_duration_since(start) >= Duration::from_millis(30));
});
}
}