Skip to main content

file_engine/
handle.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::{Duration, Instant};
5
6use tokio::task::JoinHandle;
7use tokio_stream::wrappers::UnboundedReceiverStream;
8use tokio_stream::Stream;
9use tokio_util::sync::CancellationToken;
10
11use crate::error::Result;
12use crate::progress::Progress;
13
14/// One generic type reused across every operation (`T` =
15/// `OperationOutcome` for copy/move/compress, `SyncOutcome` for sync)
16/// rather than a handle type per operation.
17pub struct Handle<T> {
18    join_handle: JoinHandle<Result<T>>,
19    progress: UnboundedReceiverStream<Progress>,
20    cancel: CancellationToken,
21    started: Instant,
22    /// Set the first time `poll` sees the task finish, so `elapsed()`
23    /// stops growing once there's no work left to time. Only reachable
24    /// for a handle polled without being consumed (`Pin::new(&mut
25    /// handle).await`, `select!`) — `handle.await` takes it by value.
26    finished: Option<Instant>,
27}
28
29impl<T> Handle<T> {
30    pub(crate) fn new(
31        join_handle: JoinHandle<Result<T>>,
32        progress: tokio::sync::mpsc::UnboundedReceiver<Progress>,
33        cancel: CancellationToken,
34    ) -> Self {
35        Self {
36            join_handle,
37            progress: UnboundedReceiverStream::new(progress),
38            cancel,
39            // Every call site constructs the `Handle` immediately after
40            // `tokio::spawn`, so this is the closest the caller can get
41            // to when the work actually began.
42            started: Instant::now(),
43            finished: None,
44        }
45    }
46
47    pub fn progress(&mut self) -> &mut (impl Stream<Item = Progress> + Unpin) {
48        &mut self.progress
49    }
50
51    /// Wall time since the operation was spawned, for displaying
52    /// "elapsed" next to `EtaEstimator`'s "remaining". Frozen once the
53    /// handle has been polled to completion, so it reads as the
54    /// operation's total duration rather than continuing to climb.
55    ///
56    /// Measures the whole run, including the directory pre-pass that
57    /// precedes the first `Started` event — so it does not line up with
58    /// a timer started on the first event off `progress()`.
59    pub fn elapsed(&self) -> Duration {
60        match self.finished {
61            Some(finished) => finished.saturating_duration_since(self.started),
62            None => self.started.elapsed(),
63        }
64    }
65
66    /// Cooperative. Detached-on-drop: dropping the `Handle` without
67    /// calling this keeps the operation running to completion. See
68    /// `docs/guide/progress-and-cancellation.md`.
69    pub fn cancel(&self) {
70        self.cancel.cancel();
71    }
72}
73
74impl<T> Future for Handle<T> {
75    type Output = Result<T>;
76
77    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
78        let this = self.get_mut();
79        match Pin::new(&mut this.join_handle).poll(cx) {
80            Poll::Ready(Ok(result)) => {
81                this.finished.get_or_insert_with(Instant::now);
82                Poll::Ready(result)
83            }
84            // The task panicked (dispatcher.rs's own internal
85            // `.expect()`s already treat that as unrecoverable) or was
86            // aborted (never happens — nothing calls `.abort()` on this
87            // JoinHandle). Propagate rather than silently swallowing it.
88            Poll::Ready(Err(join_err)) => std::panic::resume_unwind(join_err.into_panic()),
89            Poll::Pending => Poll::Pending,
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::sync::{Arc, Mutex};
97    use std::time::Duration;
98
99    use tokio::sync::mpsc;
100
101    use super::*;
102
103    #[tokio::test]
104    async fn awaiting_yields_the_wrapped_ok_value() {
105        let (_tx, rx) = mpsc::unbounded_channel();
106        let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(42) });
107        let handle = Handle::new(join_handle, rx, CancellationToken::new());
108
109        assert_eq!(handle.await.unwrap(), 42);
110    }
111
112    #[tokio::test]
113    async fn awaiting_yields_the_wrapped_err_value() {
114        let (_tx, rx) = mpsc::unbounded_channel();
115        let join_handle = tokio::spawn(async { Err::<i32, _>(crate::error::Error::Cancelled) });
116        let handle = Handle::new(join_handle, rx, CancellationToken::new());
117
118        assert!(matches!(handle.await, Err(crate::error::Error::Cancelled)));
119    }
120
121    #[tokio::test]
122    async fn progress_stream_yields_events_in_send_order_then_ends() {
123        let (tx, rx) = mpsc::unbounded_channel();
124        let reporter = crate::progress::ProgressReporter::new(tx);
125        let join_handle = tokio::spawn(async move {
126            reporter.send(Progress::Started {
127                bytes_total: None,
128                entries_total: 1,
129            });
130            reporter.send(Progress::EntryStarted {
131                entry: test_entry(),
132            });
133            // Reporter (and its clones) drop here, closing the channel.
134            Ok::<_, crate::error::Error>(())
135        });
136        let mut handle = Handle::new(join_handle, rx, CancellationToken::new());
137
138        use tokio_stream::StreamExt;
139        assert!(matches!(
140            handle.progress().next().await,
141            Some(Progress::Started { .. })
142        ));
143        assert!(matches!(
144            handle.progress().next().await,
145            Some(Progress::EntryStarted { .. })
146        ));
147        assert!(handle.progress().next().await.is_none());
148
149        handle.await.unwrap();
150    }
151
152    #[tokio::test]
153    async fn cancel_triggers_the_wrapped_cancellation_token() {
154        let (_tx, rx) = mpsc::unbounded_channel();
155        let cancel = CancellationToken::new();
156        let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(()) });
157        let handle = Handle::new(join_handle, rx, cancel.clone());
158
159        handle.cancel();
160        assert!(cancel.is_cancelled());
161    }
162
163    #[tokio::test]
164    async fn dropping_the_handle_does_not_stop_the_wrapped_task() {
165        let (_tx, rx) = mpsc::unbounded_channel();
166        let sentinel = Arc::new(Mutex::new(0));
167        let sentinel_for_task = Arc::clone(&sentinel);
168
169        let join_handle = tokio::spawn(async move {
170            *sentinel_for_task.lock().unwrap() = 1;
171            tokio::time::sleep(Duration::from_millis(30)).await;
172            *sentinel_for_task.lock().unwrap() = 2;
173            Ok::<_, crate::error::Error>(())
174        });
175        let handle = Handle::new(join_handle, rx, CancellationToken::new());
176
177        tokio::time::sleep(Duration::from_millis(10)).await;
178        assert_eq!(*sentinel.lock().unwrap(), 1);
179        drop(handle);
180
181        tokio::time::sleep(Duration::from_millis(50)).await;
182        assert_eq!(
183            *sentinel.lock().unwrap(),
184            2,
185            "task should have run to completion despite the handle being dropped"
186        );
187    }
188
189    #[tokio::test]
190    async fn elapsed_grows_while_the_operation_runs() {
191        let (_tx, rx) = mpsc::unbounded_channel();
192        let join_handle = tokio::spawn(async {
193            tokio::time::sleep(Duration::from_millis(50)).await;
194            Ok::<_, crate::error::Error>(())
195        });
196        let handle = Handle::new(join_handle, rx, CancellationToken::new());
197
198        tokio::time::sleep(Duration::from_millis(20)).await;
199        let first = handle.elapsed();
200        tokio::time::sleep(Duration::from_millis(20)).await;
201
202        assert!(first >= Duration::from_millis(20));
203        assert!(handle.elapsed() > first);
204    }
205
206    #[tokio::test]
207    async fn elapsed_freezes_once_the_task_has_completed() {
208        let (_tx, rx) = mpsc::unbounded_channel();
209        let join_handle = tokio::spawn(async {
210            tokio::time::sleep(Duration::from_millis(20)).await;
211            Ok::<_, crate::error::Error>(())
212        });
213        let mut handle = Handle::new(join_handle, rx, CancellationToken::new());
214
215        // Polled by reference rather than `handle.await`, which would
216        // consume the handle and leave nothing to call `elapsed()` on.
217        Pin::new(&mut handle).await.unwrap();
218        let at_completion = handle.elapsed();
219
220        tokio::time::sleep(Duration::from_millis(30)).await;
221
222        assert!(at_completion >= Duration::from_millis(20));
223        assert_eq!(handle.elapsed(), at_completion);
224    }
225
226    fn test_entry() -> crate::profiler::Entry {
227        crate::profiler::Entry {
228            path: std::path::PathBuf::from("a"),
229            relative_path: std::path::PathBuf::from("a"),
230            size: 1,
231            modified: None,
232        }
233    }
234}