Skip to main content

mz_ore/
task.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Tokio task utilities.
17//!
18//! ## Named task spawning
19//!
20//! The [`spawn`] and [`spawn_blocking`] methods are wrappers around
21//! [`tokio::task::spawn`] and [`tokio::task::spawn_blocking`] that attach a
22//! name the spawned task.
23//!
24//! If Clippy sent you here, replace:
25//!
26//! ```ignore
27//! tokio::task::spawn(my_future)
28//! tokio::task::spawn_blocking(my_blocking_closure)
29//! ```
30//!
31//! with:
32//!
33//! ```ignore
34//! mz_ore::task::spawn(|| format!("taskname:{}", info), my_future)
35//! mz_ore::task::spawn_blocking(|| format!("name:{}", info), my_blocking_closure)
36//! ```
37//!
38//! If you are using methods of the same names on a [`Runtime`] or [`Handle`],
39//! import [`RuntimeExt`] and replace `spawn` with [`RuntimeExt::spawn_named`]
40//! and `spawn_blocking` with [`RuntimeExt::spawn_blocking_named`], adding
41//! naming closures like above.
42
43use std::future::Future;
44use std::pin::Pin;
45use std::sync::Arc;
46use std::task::{Context, Poll};
47
48use futures::FutureExt;
49use tokio::runtime::{Handle, Runtime};
50use tokio::task::{self, JoinHandle as TokioJoinHandle};
51
52/// Wraps a [`JoinHandle`] to abort the underlying task when dropped.
53#[derive(Debug)]
54pub struct AbortOnDropHandle<T>(JoinHandle<T>);
55
56impl<T> AbortOnDropHandle<T> {
57    /// Checks if the task associated with this [`AbortOnDropHandle`] has finished.
58    pub fn is_finished(&self) -> bool {
59        self.0.inner.is_finished()
60    }
61
62    /// Aborts the task, then waits for it to release its owned resources.
63    pub async fn abort_and_wait(mut self) {
64        self.0.inner.abort();
65        let _ = (&mut self.0.inner).await;
66    }
67
68    // Note: adding an `abort(&self)` method here is incorrect; see the comment in JoinHandle::poll.
69}
70
71impl<T> Drop for AbortOnDropHandle<T> {
72    fn drop(&mut self) {
73        self.0.inner.abort();
74    }
75}
76
77impl<T> Future for AbortOnDropHandle<T> {
78    type Output = T;
79
80    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
81        self.0.poll_unpin(cx)
82    }
83}
84
85/// Wraps a tokio `JoinHandle` that has never been cancelled.
86/// This allows it to have an infallible implementation of [Future],
87/// and provides some exclusive (i.e. they take `self` ownership)
88/// operations:
89///
90/// - `abort_on_drop`: create an `AbortOnDropHandle` that will automatically abort the task
91/// when the handle is dropped.
92/// - `JoinHandleExt::abort_and_wait`: abort the task and wait for it to be finished.
93/// - `into_tokio_handle`: turn it into an ordinary tokio `JoinHandle`.
94#[derive(Debug)]
95pub struct JoinHandle<T> {
96    inner: TokioJoinHandle<T>,
97    runtime_shutting_down: bool,
98}
99
100impl<T> JoinHandle<T> {
101    /// Wrap a tokio join handle. This is intentionally private, so we can statically guarantee
102    /// that the inner join handle has not been aborted.
103    fn new(handle: TokioJoinHandle<T>) -> Self {
104        Self {
105            inner: handle,
106            runtime_shutting_down: false,
107        }
108    }
109}
110
111impl<T> Future for JoinHandle<T> {
112    type Output = T;
113
114    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
115        if self.runtime_shutting_down {
116            return Poll::Pending;
117        }
118        match self.inner.poll_unpin(cx) {
119            Poll::Ready(Ok(res)) => Poll::Ready(res),
120            Poll::Ready(Err(err)) => {
121                match err.try_into_panic() {
122                    Ok(panic) => std::panic::resume_unwind(panic),
123                    Err(err) => {
124                        assert!(
125                            err.is_cancelled(),
126                            "join errors are either cancellations or panics"
127                        );
128                        // Because `JoinHandle` and `AbortOnDropHandle` don't
129                        // offer an `abort` method, this can only happen if the runtime is
130                        // shutting down, which means this `pending` won't cause a deadlock
131                        // because Tokio drops all outstanding futures on shutdown.
132                        // (In multi-threaded runtimes, not all threads drop futures simultaneously,
133                        // so it is possible for a future on one thread to observe the drop of a future
134                        // on another thread, before it itself is dropped.)
135                        self.runtime_shutting_down = true;
136                        Poll::Pending
137                    }
138                }
139            }
140            Poll::Pending => Poll::Pending,
141        }
142    }
143}
144
145impl<T> JoinHandle<T> {
146    /// Create an [`AbortOnDropHandle`] from this [`JoinHandle`].
147    pub fn abort_on_drop(self) -> AbortOnDropHandle<T> {
148        AbortOnDropHandle(self)
149    }
150
151    /// Checks if the task associated with this [`JoinHandle`] has finished.
152    pub fn is_finished(&self) -> bool {
153        self.inner.is_finished()
154    }
155
156    /// Aborts the task, then waits for it to complete.
157    pub async fn abort_and_wait(self) {
158        self.inner.abort();
159        let _ = self.inner.await;
160    }
161
162    /// Unwrap this handle into a standard [tokio::task::JoinHandle].
163    pub fn into_tokio_handle(self) -> TokioJoinHandle<T> {
164        self.inner
165    }
166
167    // Note: adding an `abort(&self)` method here is incorrect; see the comment in JoinHandle::poll.
168}
169
170/// Spawns a new asynchronous task with a name.
171///
172/// See [`tokio::task::spawn`] and the [module][`self`] docs for more
173/// information.
174#[cfg(not(tokio_unstable))]
175#[track_caller]
176pub fn spawn<Fut, Name, NameClosure>(_nc: NameClosure, future: Fut) -> JoinHandle<Fut::Output>
177where
178    Name: AsRef<str>,
179    NameClosure: FnOnce() -> Name,
180    Fut: Future + Send + 'static,
181    Fut::Output: Send + 'static,
182{
183    // Box the future so tokio's task machinery is monomorphized over
184    // `Pin<Box<dyn Future<Output = Fut::Output> + Send>>` per output type,
185    // rather than over every distinct future type at every call site.
186    let future: Pin<Box<dyn Future<Output = Fut::Output> + Send>> = Box::pin(future);
187    #[allow(clippy::disallowed_methods)]
188    JoinHandle::new(tokio::spawn(future))
189}
190
191/// Spawns a new asynchronous task with a name.
192///
193/// See [`tokio::task::spawn`] and the [module][`self`] docs for more
194/// information.
195#[cfg(tokio_unstable)]
196#[track_caller]
197pub fn spawn<Fut, Name, NameClosure>(nc: NameClosure, future: Fut) -> JoinHandle<Fut::Output>
198where
199    Name: AsRef<str>,
200    NameClosure: FnOnce() -> Name,
201    Fut: Future + Send + 'static,
202    Fut::Output: Send + 'static,
203{
204    // Box the future so tokio's task machinery is monomorphized over
205    // `Pin<Box<dyn Future<Output = Fut::Output> + Send>>` per output type,
206    // rather than over every distinct future type at every call site.
207    let future: Pin<Box<dyn Future<Output = Fut::Output> + Send>> = Box::pin(future);
208    #[allow(clippy::disallowed_methods)]
209    JoinHandle::new(
210        task::Builder::new()
211            .name(&format!("{}:{}", Handle::current().id(), nc().as_ref()))
212            .spawn(future)
213            .expect("task spawning cannot fail"),
214    )
215}
216
217/// Runs the provided closure with a name on a thread where blocking is
218/// acceptable.
219///
220/// See [`tokio::task::spawn_blocking`] and the [module][`self`] docs for more
221/// information.
222#[cfg(not(tokio_unstable))]
223#[track_caller]
224#[allow(clippy::disallowed_methods)]
225pub fn spawn_blocking<Function, Output, Name, NameClosure>(
226    _nc: NameClosure,
227    function: Function,
228) -> JoinHandle<Output>
229where
230    Name: AsRef<str>,
231    NameClosure: FnOnce() -> Name,
232    Function: FnOnce() -> Output + Send + 'static,
233    Output: Send + 'static,
234{
235    JoinHandle::new(task::spawn_blocking(function))
236}
237
238/// Runs the provided closure with a name on a thread where blocking is
239/// acceptable.
240///
241/// See [`tokio::task::spawn_blocking`] and the [module][`self`] docs for more
242/// information.
243#[cfg(tokio_unstable)]
244#[track_caller]
245#[allow(clippy::disallowed_methods)]
246pub fn spawn_blocking<Function, Output, Name, NameClosure>(
247    nc: NameClosure,
248    function: Function,
249) -> JoinHandle<Output>
250where
251    Name: AsRef<str>,
252    NameClosure: FnOnce() -> Name,
253    Function: FnOnce() -> Output + Send + 'static,
254    Output: Send + 'static,
255{
256    JoinHandle::new(
257        task::Builder::new()
258            .name(&format!("{}:{}", Handle::current().id(), nc().as_ref()))
259            .spawn_blocking(function)
260            .expect("task spawning cannot fail"),
261    )
262}
263
264/// Extension methods for [`Runtime`] and [`Handle`].
265///
266/// See the [module][`self`] docs for more information.
267pub trait RuntimeExt {
268    /// Runs the provided closure with a name on a thread where blocking is
269    /// acceptable.
270    ///
271    /// See [`tokio::task::spawn_blocking`] and the [module][`self`] docs for more
272    /// information.
273    #[track_caller]
274    fn spawn_blocking_named<Function, Output, Name, NameClosure>(
275        &self,
276        nc: NameClosure,
277        function: Function,
278    ) -> JoinHandle<Output>
279    where
280        Name: AsRef<str>,
281        NameClosure: FnOnce() -> Name,
282        Function: FnOnce() -> Output + Send + 'static,
283        Output: Send + 'static;
284
285    /// Spawns a new asynchronous task with a name.
286    ///
287    /// See [`tokio::task::spawn`] and the [module][`self`] docs for more
288    /// information.
289    #[track_caller]
290    fn spawn_named<Fut, Name, NameClosure>(
291        &self,
292        _nc: NameClosure,
293        future: Fut,
294    ) -> JoinHandle<Fut::Output>
295    where
296        Name: AsRef<str>,
297        NameClosure: FnOnce() -> Name,
298        Fut: Future + Send + 'static,
299        Fut::Output: Send + 'static;
300}
301
302impl RuntimeExt for &Runtime {
303    fn spawn_blocking_named<Function, Output, Name, NameClosure>(
304        &self,
305        nc: NameClosure,
306        function: Function,
307    ) -> JoinHandle<Output>
308    where
309        Name: AsRef<str>,
310        NameClosure: FnOnce() -> Name,
311        Function: FnOnce() -> Output + Send + 'static,
312        Output: Send + 'static,
313    {
314        let _g = self.enter();
315        spawn_blocking(nc, function)
316    }
317
318    fn spawn_named<Fut, Name, NameClosure>(
319        &self,
320        nc: NameClosure,
321        future: Fut,
322    ) -> JoinHandle<Fut::Output>
323    where
324        Name: AsRef<str>,
325        NameClosure: FnOnce() -> Name,
326        Fut: Future + Send + 'static,
327        Fut::Output: Send + 'static,
328    {
329        let _g = self.enter();
330        spawn(nc, future)
331    }
332}
333
334impl RuntimeExt for Arc<Runtime> {
335    fn spawn_blocking_named<Function, Output, Name, NameClosure>(
336        &self,
337        nc: NameClosure,
338        function: Function,
339    ) -> JoinHandle<Output>
340    where
341        Name: AsRef<str>,
342        NameClosure: FnOnce() -> Name,
343        Function: FnOnce() -> Output + Send + 'static,
344        Output: Send + 'static,
345    {
346        (&**self).spawn_blocking_named(nc, function)
347    }
348
349    fn spawn_named<Fut, Name, NameClosure>(
350        &self,
351        nc: NameClosure,
352        future: Fut,
353    ) -> JoinHandle<Fut::Output>
354    where
355        Name: AsRef<str>,
356        NameClosure: FnOnce() -> Name,
357        Fut: Future + Send + 'static,
358        Fut::Output: Send + 'static,
359    {
360        (&**self).spawn_named(nc, future)
361    }
362}
363
364impl RuntimeExt for Handle {
365    fn spawn_blocking_named<Function, Output, Name, NameClosure>(
366        &self,
367        nc: NameClosure,
368        function: Function,
369    ) -> JoinHandle<Output>
370    where
371        Name: AsRef<str>,
372        NameClosure: FnOnce() -> Name,
373        Function: FnOnce() -> Output + Send + 'static,
374        Output: Send + 'static,
375    {
376        let _g = self.enter();
377        spawn_blocking(nc, function)
378    }
379
380    fn spawn_named<Fut, Name, NameClosure>(
381        &self,
382        nc: NameClosure,
383        future: Fut,
384    ) -> JoinHandle<Fut::Output>
385    where
386        Name: AsRef<str>,
387        NameClosure: FnOnce() -> Name,
388        Fut: Future + Send + 'static,
389        Fut::Output: Send + 'static,
390    {
391        let _g = self.enter();
392        spawn(nc, future)
393    }
394}
395
396/// Extension methods for [`tokio::task::JoinSet`].
397///
398/// See the [module][`self`] docs for more information.
399pub trait JoinSetExt<T> {
400    /// Spawns a new asynchronous task with a name.
401    ///
402    /// See [`tokio::task::spawn`] and the [module][`self`] docs for more
403    /// information.
404    #[track_caller]
405    fn spawn_named<Fut, Name, NameClosure>(
406        &mut self,
407        nc: NameClosure,
408        future: Fut,
409    ) -> tokio::task::AbortHandle
410    where
411        Name: AsRef<str>,
412        NameClosure: FnOnce() -> Name,
413        Fut: Future<Output = T> + Send + 'static,
414        T: Send + 'static;
415}
416
417impl<T> JoinSetExt<T> for tokio::task::JoinSet<T> {
418    // Allow unused variables until everything in ci uses `tokio_unstable`.
419    #[allow(unused_variables)]
420    fn spawn_named<Fut, Name, NameClosure>(
421        &mut self,
422        nc: NameClosure,
423        future: Fut,
424    ) -> tokio::task::AbortHandle
425    where
426        Name: AsRef<str>,
427        NameClosure: FnOnce() -> Name,
428        Fut: Future<Output = T> + Send + 'static,
429        T: Send + 'static,
430    {
431        // Box the future so tokio's task machinery is monomorphized over
432        // `Pin<Box<dyn Future<Output = T> + Send>>` per output type, rather
433        // than over every distinct future type at every call site. See the
434        // analogous comment on the top-level `spawn` for context.
435        let future: Pin<Box<dyn Future<Output = T> + Send>> = Box::pin(future);
436        #[cfg(tokio_unstable)]
437        #[allow(clippy::disallowed_methods)]
438        {
439            self.build_task()
440                .name(&format!("{}:{}", Handle::current().id(), nc().as_ref()))
441                .spawn(future)
442                .expect("task spawning cannot fail")
443        }
444        #[cfg(not(tokio_unstable))]
445        #[allow(clippy::disallowed_methods)]
446        {
447            self.spawn(future)
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use std::future;
455    use std::sync::Arc;
456    use std::sync::atomic::{AtomicBool, Ordering};
457
458    use tokio::sync::oneshot;
459
460    struct SetOnDrop(Arc<AtomicBool>);
461
462    impl Drop for SetOnDrop {
463        fn drop(&mut self) {
464            self.0.store(true, Ordering::SeqCst);
465        }
466    }
467
468    #[mz_ore::test(tokio::test)]
469    async fn abort_on_drop_handle_waits_for_task_drop() {
470        let dropped = Arc::new(AtomicBool::new(false));
471        let guard = SetOnDrop(Arc::clone(&dropped));
472        let (started_tx, started_rx) = oneshot::channel();
473        let handle = super::spawn(|| "abort_on_drop_handle_waits_for_task_drop", async move {
474            let _guard = guard;
475            started_tx.send(()).expect("receiver remains live");
476            future::pending::<()>().await;
477        })
478        .abort_on_drop();
479
480        started_rx.await.expect("task starts");
481        handle.abort_and_wait().await;
482
483        assert!(dropped.load(Ordering::SeqCst));
484    }
485}