Skip to main content

rama_net/proxy/
idle.rs

1//! [`IdleGuard`] — a small helper for detecting idleness in a `tokio::select!` loop.
2
3use std::pin::Pin;
4use std::time::Duration;
5
6use tokio::time::{Instant, Sleep};
7
8/// A resettable idle deadline.
9///
10/// Designed to be used as one arm of a `tokio::select!`: poll [`IdleGuard::tick`]
11/// in the select; when another arm fires (i.e. progress was observed) call
12/// [`IdleGuard::reset`] before re-entering the select to extend the idle window.
13///
14/// If the inner [`Sleep`] elapses before [`reset`](IdleGuard::reset) is called,
15/// the idle window has lapsed.
16///
17/// `IdleGuard` is intended for cases where the watched activity does not itself
18/// produce values that can be selected on (e.g. byte progress observed inside
19/// another future). For values that can be selected on, prefer racing the
20/// activity directly.
21pub struct IdleGuard {
22    timeout: Duration,
23    sleep: Pin<Box<Sleep>>,
24}
25
26impl core::fmt::Debug for IdleGuard {
27    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28        f.debug_struct("IdleGuard")
29            .field("timeout", &self.timeout)
30            .finish_non_exhaustive()
31    }
32}
33
34impl IdleGuard {
35    /// Create a new [`IdleGuard`] that fires after `timeout` of inactivity.
36    #[must_use]
37    pub fn new(timeout: Duration) -> Self {
38        Self {
39            timeout,
40            sleep: Box::pin(tokio::time::sleep(timeout)),
41        }
42    }
43
44    /// The configured idle timeout.
45    #[must_use]
46    pub fn timeout(&self) -> Duration {
47        self.timeout
48    }
49
50    /// Reset the idle deadline to `now + timeout`. Call this whenever progress
51    /// has been observed, before re-entering the select that polls
52    /// [`IdleGuard::tick`].
53    pub fn reset(&mut self) {
54        let deadline = Instant::now() + self.timeout;
55        self.sleep.as_mut().reset(deadline);
56    }
57
58    /// Poll-able future that completes when the idle window has elapsed.
59    ///
60    /// Borrow-and-poll inside `tokio::select!`. Once it completes, the guard
61    /// is considered tripped — call [`IdleGuard::reset`] before re-arming if
62    /// you want to keep waiting.
63    pub fn tick(&mut self) -> &mut Pin<Box<Sleep>> {
64        &mut self.sleep
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use std::time::Duration;
71
72    use super::*;
73
74    #[tokio::test(start_paused = true)]
75    async fn idle_guard_fires_after_timeout() {
76        let mut guard = IdleGuard::new(Duration::from_millis(100));
77        tokio::time::advance(Duration::from_millis(101)).await;
78        // Should be ready immediately after enough virtual time has passed.
79        guard.tick().await;
80    }
81
82    #[tokio::test(start_paused = true)]
83    async fn idle_guard_reset_extends_window() {
84        let mut guard = IdleGuard::new(Duration::from_millis(100));
85
86        // Advance most of the way, then reset.
87        tokio::time::advance(Duration::from_millis(80)).await;
88        guard.reset();
89
90        // Advance another 80ms — would have fired without reset.
91        tokio::time::advance(Duration::from_millis(80)).await;
92
93        // Race a short timeout against the guard; the guard should NOT have
94        // fired yet (we've only spent 80ms since reset out of 100ms window).
95        tokio::select! {
96            biased;
97            _ = guard.tick() => panic!("idle guard fired prematurely"),
98            _ = tokio::time::sleep(Duration::from_millis(0)) => {}
99        }
100
101        // Advance past the reset deadline.
102        tokio::time::advance(Duration::from_millis(30)).await;
103        guard.tick().await;
104    }
105}