Skip to main content

videre_core/
io_timeout.rs

1use std::sync::mpsc;
2use std::thread;
3use std::time::{Duration, Instant};
4
5/// Default ceiling for any single blocking file/subprocess operation that
6/// touches a path supplied by the caller (e.g. a scanned media file). Chosen
7/// to comfortably exceed a slow spinning disk or network share while still
8/// surfacing a stale/disconnected mount point (which otherwise blocks the
9/// underlying syscall forever on macOS) within one command's run.
10pub const DEFAULT_IO_TIMEOUT: Duration = Duration::from_secs(20);
11
12/// The operation did not complete within the given timeout. The spawned
13/// worker thread is left to run to completion in the background (there is
14/// no safe way to cancel a blocked syscall from the outside); this trades a
15/// leaked thread for never hanging the caller.
16pub struct TimedOut;
17
18/// Runs `f` on a helper thread and waits up to `timeout` for it to finish.
19/// Use this to bound any blocking call (`std::fs::*`, `image::open`, a
20/// subprocess `.wait()`) that could otherwise block indefinitely against an
21/// unresponsive mount point.
22pub fn run_with_timeout<T, F>(timeout: Duration, f: F) -> Result<T, TimedOut>
23where
24    F: FnOnce() -> T + Send + 'static,
25    T: Send + 'static,
26{
27    let (tx, rx) = mpsc::channel();
28    thread::spawn(move || {
29        let _ = tx.send(f());
30    });
31    rx.recv_timeout(timeout).map_err(|_| TimedOut)
32}
33
34/// Outcome of waiting on a child process with a deadline.
35#[derive(Debug, PartialEq, Eq)]
36pub enum WaitOutcome {
37    Success,
38    Failed,
39    TimedOut,
40}
41
42/// Polls `child` for completion, killing it if it hasn't exited within
43/// `timeout`. Unlike a raw blocking `.wait()`/`.status()`, this guarantees
44/// the caller gets control back within roughly `timeout` even if the child
45/// itself is stuck on an unresponsive mount point.
46pub fn wait_with_timeout(child: &mut std::process::Child, timeout: Duration) -> WaitOutcome {
47    let start = Instant::now();
48    loop {
49        match child.try_wait() {
50            Ok(Some(status)) => {
51                return if status.success() { WaitOutcome::Success } else { WaitOutcome::Failed };
52            }
53            Ok(None) => {
54                if start.elapsed() >= timeout {
55                    let _ = child.kill();
56                    let _ = child.wait();
57                    return WaitOutcome::TimedOut;
58                }
59                thread::sleep(Duration::from_millis(50));
60            }
61            Err(_) => return WaitOutcome::Failed,
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn returns_ok_when_operation_finishes_before_timeout() {
72        let result = run_with_timeout(Duration::from_secs(1), || 42);
73        assert!(result.is_ok());
74        assert_eq!(result.ok(), Some(42));
75    }
76
77    #[test]
78    fn returns_timed_out_when_operation_exceeds_timeout() {
79        let result = run_with_timeout(Duration::from_millis(50), || {
80            thread::sleep(Duration::from_secs(5));
81            42
82        });
83        assert!(result.is_err());
84    }
85
86    #[test]
87    fn wait_with_timeout_returns_success_for_fast_process() {
88        let mut child = std::process::Command::new("true").spawn().unwrap();
89        assert_eq!(wait_with_timeout(&mut child, Duration::from_secs(5)), WaitOutcome::Success);
90    }
91
92    #[test]
93    fn wait_with_timeout_kills_and_returns_timed_out_for_slow_process() {
94        let mut child = std::process::Command::new("sleep").arg("5").spawn().unwrap();
95        let start = Instant::now();
96        assert_eq!(wait_with_timeout(&mut child, Duration::from_millis(200)), WaitOutcome::TimedOut);
97        assert!(start.elapsed() < Duration::from_secs(2));
98    }
99}