Skip to main content

conmonrs/streaming_server/
idle.rs

1//! The idle timeout of streaming connections.
2
3use crate::mutex::lock;
4use std::{
5    future::{self, Future},
6    sync::{Arc, Mutex},
7};
8use tokio::{
9    select, task,
10    time::{self, Duration, Instant},
11};
12use tokio_util::sync::CancellationToken;
13
14/// How long the end of a connection waits for a client which takes nothing
15/// of what the server sends, like the rest of the output, the status of a
16/// session and the close, before the connection gets ended. A slow client
17/// gets all of it, only one which stopped reading gets dropped, see
18/// [`Activity::until_stalled`]. Not too short: the kernel wakes up a write
19/// which waits for the client only once a third of the send buffer drained,
20/// which can take a while with a send buffer of a few MiB and a slow
21/// client.
22pub(super) const STALL_TIMEOUT: Duration = if cfg!(test) {
23    Duration::from_secs(5)
24} else {
25    Duration::from_secs(60)
26};
27
28/// When something last made progress, like a connection by what its client
29/// sent or got, or a container which took the stdin of a session. Clones
30/// share the time.
31#[derive(Clone, Debug)]
32pub(super) struct Activity(Arc<Mutex<Instant>>);
33
34impl Activity {
35    /// An activity whose last progress is now.
36    pub(super) fn new() -> Self {
37        Self(Arc::new(Mutex::new(Instant::now())))
38    }
39
40    /// Something made progress now.
41    pub(super) fn touch(&self) {
42        *lock(&self.0) = Instant::now();
43    }
44
45    /// When something last made progress.
46    pub(super) fn last(&self) -> Instant {
47        *lock(&self.0)
48    }
49
50    /// Resolves once the connection was not active for the timeout. A timeout
51    /// too large to represent is none, like no timeout.
52    pub(super) async fn idle(&self, timeout: Option<Duration>) {
53        let Some(timeout) = timeout else {
54            return future::pending().await;
55        };
56        loop {
57            let Some(deadline) = self.last().checked_add(timeout) else {
58                return future::pending().await;
59            };
60            if Instant::now() >= deadline {
61                return;
62            }
63            time::sleep_until(deadline).await;
64        }
65    }
66
67    /// Run `run` until it is done, or until the connection was not active
68    /// for the timeout, which gives `None`. `on_idle` runs before `run` gets
69    /// dropped then.
70    pub(super) async fn until_idle<T>(
71        &self,
72        timeout: Option<Duration>,
73        run: impl Future<Output = T>,
74        on_idle: impl FnOnce(),
75    ) -> Option<T> {
76        let idle = async {
77            self.idle(timeout).await;
78            on_idle();
79        };
80        select! {
81            output = run => Some(output),
82            () = idle => None,
83        }
84    }
85
86    /// Run `run` until it is done, or until the activity was not touched
87    /// for the `timeout`, counted from `start` at the earliest, which gives
88    /// `None`. The writes of a connection touch it as they go out, which
89    /// lets the end of a connection take as long as a slow client needs,
90    /// see [`STALL_TIMEOUT`].
91    pub(super) async fn until_stalled<T>(
92        &self,
93        start: Instant,
94        timeout: Duration,
95        run: impl Future<Output = T>,
96    ) -> Option<T> {
97        self.until_stalled_or_idle(start, timeout, None, run).await
98    }
99
100    /// Like [`Self::until_stalled`], but not before the activity was not
101    /// touched for the idle timeout either. A client which pauses the output,
102    /// like a pager, keeps the end as long as it kept the rest of a session,
103    /// one which ended for its idle timeout gets only the `timeout`, like
104    /// with an idle timeout too large for the clock.
105    pub(super) async fn until_stalled_or_idle<T>(
106        &self,
107        start: Instant,
108        timeout: Duration,
109        idle_timeout: Option<Duration>,
110        run: impl Future<Output = T>,
111    ) -> Option<T> {
112        let deadline = |last: Instant| {
113            let stalled = last.max(start).checked_add(timeout)?;
114            let idle = idle_timeout.and_then(|idle_timeout| last.checked_add(idle_timeout));
115            Some(idle.map_or(stalled, |idle| stalled.max(idle)))
116        };
117        let stalled = async {
118            loop {
119                let Some(deadline) = deadline(self.last()) else {
120                    return future::pending().await;
121                };
122                if Instant::now() >= deadline {
123                    return;
124                }
125                time::sleep_until(deadline).await;
126            }
127        };
128        select! {
129            biased;
130            output = run => Some(output),
131            () = stalled => None,
132        }
133    }
134
135    /// Watch the connection in a task of its own until `ended` gets
136    /// cancelled, and run `on_idle` once it was not active for the timeout
137    /// before that.
138    pub(super) fn watch(
139        &self,
140        timeout: Option<Duration>,
141        ended: CancellationToken,
142        on_idle: impl FnOnce() + Send + 'static,
143    ) {
144        let activity = self.clone();
145        task::spawn(async move {
146            if activity
147                .until_idle(timeout, ended.cancelled(), || {})
148                .await
149                .is_none()
150            {
151                on_idle();
152            }
153        });
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[tokio::test(start_paused = true)]
162    async fn idle_after_the_timeout() {
163        let activity = Activity::new();
164        let timeout = Some(Duration::from_secs(60));
165        time::sleep(Duration::from_secs(50)).await;
166        activity.touch();
167        assert!(
168            time::timeout(Duration::from_secs(59), activity.idle(timeout))
169                .await
170                .is_err()
171        );
172        time::timeout(Duration::from_secs(2), activity.idle(timeout))
173            .await
174            .expect("idle");
175    }
176
177    #[tokio::test(start_paused = true)]
178    async fn runs_end_with_the_idle_timeout() {
179        let activity = Activity::new();
180        let timeout = Some(Duration::from_secs(60));
181        let done = activity
182            .until_idle(timeout, time::sleep(Duration::from_secs(30)), || {
183                panic!("idle")
184            })
185            .await;
186        assert_eq!(done, Some(()));
187
188        // The run sees the idle callback before it gets dropped.
189        let idle = Arc::new(Mutex::new(false));
190        struct Check(Arc<Mutex<bool>>);
191        impl Drop for Check {
192            fn drop(&mut self) {
193                assert!(*self.0.lock().unwrap());
194            }
195        }
196        let check = Check(idle.clone());
197        let run = async move {
198            let _check = check;
199            future::pending::<()>().await;
200        };
201        let start = Instant::now();
202        let result = activity
203            .until_idle(timeout, run, || *idle.lock().unwrap() = true)
204            .await;
205        assert_eq!(result, None);
206        assert_eq!(start.elapsed(), Duration::from_secs(30));
207    }
208
209    #[tokio::test(start_paused = true)]
210    async fn only_stalls_end_runs() {
211        let activity = Activity::new();
212        let timeout = Duration::from_secs(5);
213        // An activity from long ago does not count against a run which
214        // starts now.
215        time::sleep(Duration::from_secs(60)).await;
216        let start = Instant::now();
217        let progress = async {
218            for _ in 0..10 {
219                time::sleep(Duration::from_secs(4)).await;
220                activity.touch();
221            }
222        };
223        assert_eq!(
224            activity.until_stalled(start, timeout, progress).await,
225            Some(())
226        );
227        assert_eq!(start.elapsed(), Duration::from_secs(40));
228
229        // A run which stops making progress ends after the timeout.
230        let start = Instant::now();
231        let stalls = async {
232            time::sleep(Duration::from_secs(3)).await;
233            activity.touch();
234            future::pending::<()>().await;
235        };
236        assert_eq!(activity.until_stalled(start, timeout, stalls).await, None);
237        assert_eq!(start.elapsed(), Duration::from_secs(8));
238    }
239
240    #[tokio::test(start_paused = true)]
241    async fn stalls_end_runs_after_the_idle_timeout() {
242        let activity = Activity::new();
243        let timeout = Duration::from_secs(5);
244        let idle_timeout = Some(Duration::from_secs(60));
245
246        // A client which got something 20 seconds ago, then paused, gets the
247        // rest of the idle timeout.
248        time::sleep(Duration::from_secs(20)).await;
249        let start = Instant::now();
250        let stalls = future::pending::<()>();
251        assert_eq!(
252            activity
253                .until_stalled_or_idle(start, timeout, idle_timeout, stalls)
254                .await,
255            None
256        );
257        assert_eq!(start.elapsed(), Duration::from_secs(40));
258
259        // One which got nothing for the idle timeout still gets the timeout.
260        let start = Instant::now();
261        let stalls = future::pending::<()>();
262        assert_eq!(
263            activity
264                .until_stalled_or_idle(start, timeout, idle_timeout, stalls)
265                .await,
266            None
267        );
268        assert_eq!(start.elapsed(), timeout);
269
270        // Each part which goes out gives it the idle timeout again.
271        activity.touch();
272        let start = Instant::now();
273        let progress = async {
274            for _ in 0..3 {
275                time::sleep(Duration::from_secs(50)).await;
276                activity.touch();
277            }
278        };
279        assert_eq!(
280            activity
281                .until_stalled_or_idle(start, timeout, idle_timeout, progress)
282                .await,
283            Some(())
284        );
285        assert_eq!(start.elapsed(), Duration::from_secs(150));
286    }
287
288    #[tokio::test(start_paused = true)]
289    async fn huge_idle_timeouts_leave_the_stall_timeout() {
290        let activity = Activity::new();
291        let timeout = Duration::from_secs(5);
292        let start = Instant::now();
293        let stalls = future::pending::<()>();
294        let ended = activity.until_stalled_or_idle(start, timeout, Some(Duration::MAX), stalls);
295        assert_eq!(time::timeout(10 * timeout, ended).await, Ok(None));
296        assert_eq!(start.elapsed(), timeout);
297    }
298
299    #[tokio::test(start_paused = true)]
300    async fn huge_timeouts_are_none() {
301        let activity = Activity::new();
302        for timeout in [
303            None,
304            Some(Duration::MAX),
305            Some(Duration::from_secs(u64::MAX)),
306        ] {
307            assert!(
308                time::timeout(Duration::from_secs(3600), activity.idle(timeout))
309                    .await
310                    .is_err()
311            );
312        }
313    }
314}