use std::future::poll_fn;
use std::pin::{Pin, pin};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::task::Poll;
use std::time::Duration;
use crate::core::{BoxFuture, Core};
#[derive(Clone, Debug)]
pub struct Progress {
ticks: Arc<AtomicU64>,
}
impl Progress {
fn new() -> Self {
Self {
ticks: Arc::new(AtomicU64::new(0)),
}
}
pub fn tick(&self) {
self.ticks.fetch_add(1, Ordering::Relaxed);
}
fn sample(&self) -> u64 {
self.ticks.load(Ordering::Relaxed)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum StallError<E> {
Stalled { idle: Duration },
Operation(E),
}
pub async fn stall_timeout<C, T, E, F>(
core: &C,
budget: Duration,
op: F,
) -> Result<T, StallError<E>>
where
C: Core,
F: AsyncFnOnce(Progress) -> Result<T, E>,
{
let progress = Progress::new();
let op_fut = op(progress.clone());
let mut op_fut = pin!(op_fut);
let mut last_ticks = progress.sample();
let mut deadline = core.now() + budget;
let mut sleep: Option<BoxFuture<'_, ()>> = None;
poll_fn(|cx| {
if let Poll::Ready(r) = op_fut.as_mut().poll(cx) {
return Poll::Ready(r.map_err(StallError::Operation));
}
let now_ticks = progress.sample();
if now_ticks != last_ticks {
last_ticks = now_ticks;
deadline = core.now() + budget;
sleep = None; }
let s =
sleep.get_or_insert_with(|| core.sleep(deadline.saturating_duration_since(core.now())));
if Pin::new(s).poll(cx).is_ready() {
return Poll::Ready(Err(StallError::Stalled { idle: budget }));
}
Poll::Pending
})
.await
}
#[cfg(all(test, feature = "test-util"))]
mod tests {
use super::*;
use crate::core::{ManualClock, TestCore};
#[tokio::test]
async fn slow_but_ticking_op_is_not_killed() {
let clock = ManualClock::new();
let core = TestCore::new(clock.clone());
let out: Result<u32, StallError<()>> =
stall_timeout(&core, Duration::from_millis(100), async |p: Progress| {
for _ in 0..5 {
clock.advance(Duration::from_millis(40)); p.tick();
tokio::task::yield_now().await;
}
Ok(7)
})
.await;
assert_eq!(out, Ok(7));
}
#[tokio::test]
async fn silent_op_is_killed() {
let clock = ManualClock::new();
let core = TestCore::new(clock.clone());
let out: Result<u32, StallError<()>> =
stall_timeout(&core, Duration::from_millis(100), async |_p: Progress| {
clock.advance(Duration::from_millis(150)); std::future::pending::<()>().await;
Ok(0)
})
.await;
assert_eq!(
out,
Err(StallError::Stalled {
idle: Duration::from_millis(100)
})
);
}
}