use std::{
error, fmt,
future::{Future, poll_fn},
io,
pin::Pin,
task::{Context, Poll},
time::{Duration, Instant},
};
use futures_core::Stream;
use crate::{Local, Mode, reactor};
#[must_use = "futures do nothing unless .awaited"]
pub struct Timeout<F, M = Local>
where
M: Mode,
{
future: F,
sleep: Sleep<M>,
}
impl<F, M> Timeout<F, M>
where
M: Mode,
{
pub fn get_ref(&self) -> &F {
&self.future
}
pub fn get_mut(&mut self) -> &mut F {
&mut self.future
}
pub fn into_inner(self) -> F {
self.future
}
pub(crate) fn new(future: F, sleep: Sleep<M>) -> Self {
Self { future, sleep }
}
}
impl<F, M> fmt::Debug for Timeout<F, M>
where
M: Mode,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Timeout")
.field("deadline", &self.sleep.deadline())
.finish_non_exhaustive()
}
}
impl<F, M> Future for Timeout<F, M>
where
F: Future,
M: Mode,
{
type Output = Result<F::Output, TimedOut>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let (future, sleep) = unsafe {
let this = self.get_unchecked_mut();
(Pin::new_unchecked(&mut this.future), &mut this.sleep)
};
if let Poll::Ready(output) = future.poll(cx) {
return Poll::Ready(Ok(output));
}
if Pin::new(sleep).poll(cx).is_ready() {
return Poll::Ready(Err(TimedOut));
}
Poll::Pending
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct TimedOut;
impl fmt::Display for TimedOut {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the deadline passed before the future completed")
}
}
impl error::Error for TimedOut {}
impl From<TimedOut> for io::Error {
fn from(timed_out: TimedOut) -> Self {
io::Error::new(io::ErrorKind::TimedOut, timed_out)
}
}
pub struct Interval<M = Local>
where
M: Mode,
{
sleep: Sleep<M>,
period: Duration,
missed_tick_behavior: MissedTickBehavior,
}
impl<M> Interval<M>
where
M: Mode,
{
pub async fn tick(&mut self) -> Instant {
poll_fn(|cx| self.poll_tick(cx)).await
}
pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll<Instant> {
let Some(deadline) = self.sleep.deadline() else {
return Poll::Pending;
};
if Pin::new(&mut self.sleep).poll(cx).is_pending() {
return Poll::Pending;
}
let next = next_deadline(
deadline,
Instant::now(),
self.period,
self.missed_tick_behavior,
);
self.sleep.0.reset(next);
Poll::Ready(deadline)
}
pub fn reset(&mut self) {
self.sleep.reset_after(self.period);
}
pub fn period(&self) -> Duration {
self.period
}
pub fn missed_tick_behavior(&self) -> MissedTickBehavior {
self.missed_tick_behavior
}
pub fn set_missed_tick_behavior(&mut self, behavior: MissedTickBehavior) {
self.missed_tick_behavior = behavior;
}
pub(crate) fn new(sleep: Sleep<M>, period: Duration) -> Self {
assert!(!period.is_zero(), "an interval's period must not be zero");
Self {
sleep,
period,
missed_tick_behavior: MissedTickBehavior::default(),
}
}
}
impl<M> fmt::Debug for Interval<M>
where
M: Mode,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Interval")
.field("next_tick", &self.sleep.deadline())
.field("period", &self.period)
.field("missed_tick_behavior", &self.missed_tick_behavior)
.finish()
}
}
impl<M> Stream for Interval<M>
where
M: Mode,
{
type Item = Instant;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Instant>> {
self.get_mut().poll_tick(cx).map(Some)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum MissedTickBehavior {
#[default]
Burst,
Delay,
Skip,
}
#[must_use = "futures do nothing unless .awaited"]
pub struct Sleep<M = Local>(pub(crate) reactor::Sleep<M>)
where
M: Mode;
impl<M> Sleep<M>
where
M: Mode,
{
pub fn deadline(&self) -> Option<Instant> {
self.0.deadline()
}
pub fn reset(&mut self, deadline: Instant) {
self.0.reset(Some(deadline));
}
pub fn reset_after(&mut self, duration: Duration) {
self.0.reset(Instant::now().checked_add(duration));
}
}
impl<M> Future for Sleep<M>
where
M: Mode,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.get_mut();
if Pin::new(&mut this.0).poll(cx).is_ready() {
return Poll::Ready(());
}
if this.0.never_fires() {
return Poll::Pending;
}
M::ensure_progress(this.0.core());
Poll::Pending
}
}
fn next_deadline(
deadline: Instant,
now: Instant,
period: Duration,
behavior: MissedTickBehavior,
) -> Option<Instant> {
let next = deadline.checked_add(period)?;
if next > now {
return Some(next);
}
match behavior {
MissedTickBehavior::Burst => Some(next),
MissedTickBehavior::Delay => now.checked_add(period),
MissedTickBehavior::Skip => {
let period = period.as_nanos();
let rem = (now - deadline).as_nanos() % period;
let gap = period - rem;
let gap = Duration::new((gap / NANOS_PER_SEC) as u64, (gap % NANOS_PER_SEC) as u32);
now.checked_add(gap)
}
}
}
const NANOS_PER_SEC: u128 = 1_000_000_000;
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use ntest::timeout;
use super::{MissedTickBehavior, next_deadline};
const PERIOD: Duration = Duration::from_millis(10);
#[test]
#[timeout(15000)]
fn nothing_missed_keeps_the_schedule() {
let base = Instant::now();
let deadline = base + ms(10);
let now = base + ms(15);
for behavior in BEHAVIORS {
assert_eq!(
next_deadline(deadline, now, PERIOD, behavior),
Some(base + ms(20)),
"{behavior:?}",
);
}
}
#[test]
#[timeout(15000)]
fn a_tick_handed_out_at_the_next_ones_moment_has_missed_it() {
let base = Instant::now();
let deadline = base + ms(10);
let now = base + ms(20);
assert_eq!(
next_deadline(deadline, now, PERIOD, MissedTickBehavior::Delay),
Some(base + ms(30)),
);
assert_eq!(
next_deadline(deadline, now, PERIOD, MissedTickBehavior::Skip),
Some(base + ms(30)),
);
}
#[test]
#[timeout(15000)]
fn burst_hands_out_each_missed_tick() {
let base = Instant::now();
assert_eq!(
next_deadline(
base + ms(20),
base + ms(35),
PERIOD,
MissedTickBehavior::Burst
),
Some(base + ms(30)),
);
assert_eq!(
next_deadline(
base + ms(20),
base + ms(75),
PERIOD,
MissedTickBehavior::Burst
),
Some(base + ms(30)),
);
}
#[test]
#[timeout(15000)]
fn delay_moves_the_schedule_to_a_period_after_now() {
let base = Instant::now();
assert_eq!(
next_deadline(
base + ms(20),
base + ms(35),
PERIOD,
MissedTickBehavior::Delay
),
Some(base + ms(45)),
);
assert_eq!(
next_deadline(
base + ms(20),
base + ms(75),
PERIOD,
MissedTickBehavior::Delay
),
Some(base + ms(85)),
);
}
#[test]
#[timeout(15000)]
fn skip_moves_on_to_the_first_tick_still_to_come() {
let base = Instant::now();
assert_eq!(
next_deadline(
base + ms(20),
base + ms(35),
PERIOD,
MissedTickBehavior::Skip
),
Some(base + ms(40)),
);
assert_eq!(
next_deadline(
base + ms(20),
base + ms(75),
PERIOD,
MissedTickBehavior::Skip
),
Some(base + ms(80)),
);
assert_eq!(
next_deadline(
base + ms(20),
base + ms(75) + Duration::from_nanos(1),
PERIOD,
MissedTickBehavior::Skip,
),
Some(base + ms(80)),
);
}
#[test]
#[timeout(15000)]
fn skip_on_a_tick_of_the_schedule_moves_a_full_period_on() {
let base = Instant::now();
assert_eq!(
next_deadline(
base + ms(20),
base + ms(50),
PERIOD,
MissedTickBehavior::Skip
),
Some(base + ms(60)),
);
}
#[test]
#[timeout(15000)]
fn skip_keeps_the_seconds_of_a_long_period() {
let base = Instant::now();
let period = Duration::new(3, 500_000_000);
let deadline = base + period;
let now = deadline + period * 2 + Duration::from_millis(1_750);
assert_eq!(
next_deadline(deadline, now, period, MissedTickBehavior::Skip),
Some(deadline + period * 3),
);
}
#[test]
#[timeout(15000)]
fn no_next_tick_beyond_the_clock() {
let base = Instant::now();
for behavior in BEHAVIORS {
assert_eq!(
next_deadline(base, base, Duration::MAX, behavior),
None,
"{behavior:?}",
);
}
}
const BEHAVIORS: [MissedTickBehavior; 3] = [
MissedTickBehavior::Burst,
MissedTickBehavior::Delay,
MissedTickBehavior::Skip,
];
fn ms(millis: u64) -> Duration {
Duration::from_millis(millis)
}
}