tokio_util/task/
task_tracker.rs

1//! Types related to the [`TaskTracker`] collection.
2//!
3//! See the documentation of [`TaskTracker`] for more information.
4
5use pin_project_lite::pin_project;
6use std::fmt;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::task::{Context, Poll};
12use tokio::sync::{futures::Notified, Notify};
13
14#[cfg(feature = "rt")]
15use tokio::{
16    runtime::Handle,
17    task::{JoinHandle, LocalSet},
18};
19
20/// A task tracker used for waiting until tasks exit.
21///
22/// This is usually used together with [`CancellationToken`] to implement [graceful shutdown]. The
23/// `CancellationToken` is used to signal to tasks that they should shut down, and the
24/// `TaskTracker` is used to wait for them to finish shutting down.
25///
26/// The `TaskTracker` will also keep track of a `closed` boolean. This is used to handle the case
27/// where the `TaskTracker` is empty, but we don't want to shut down yet. This means that the
28/// [`wait`] method will wait until *both* of the following happen at the same time:
29///
30///  * The `TaskTracker` must be closed using the [`close`] method.
31///  * The `TaskTracker` must be empty, that is, all tasks that it is tracking must have exited.
32///
33/// When a call to [`wait`] returns, it is guaranteed that all tracked tasks have exited and that
34/// the destructor of the future has finished running. However, there might be a short amount of
35/// time where [`JoinHandle::is_finished`] returns false.
36///
37/// # Comparison to `JoinSet`
38///
39/// The main Tokio crate has a similar collection known as [`JoinSet`]. The `JoinSet` type has a
40/// lot more features than `TaskTracker`, so `TaskTracker` should only be used when one of its
41/// unique features is required:
42///
43///  1. When tasks exit, a `TaskTracker` will allow the task to immediately free its memory.
44///  2. By not closing the `TaskTracker`, [`wait`] will be prevented from returning even if
45///     the `TaskTracker` is empty.
46///  3. A `TaskTracker` does not require mutable access to insert tasks.
47///  4. A `TaskTracker` can be cloned to share it with many tasks.
48///
49/// The first point is the most important one. A [`JoinSet`] keeps track of the return value of
50/// every inserted task. This means that if the caller keeps inserting tasks and never calls
51/// [`join_next`], then their return values will keep building up and consuming memory, _even if_
52/// most of the tasks have already exited. This can cause the process to run out of memory. With a
53/// `TaskTracker`, this does not happen. Once tasks exit, they are immediately removed from the
54/// `TaskTracker`.
55///
56/// # Examples
57///
58/// For more examples, please see the topic page on [graceful shutdown].
59///
60/// ## Spawn tasks and wait for them to exit
61///
62/// This is a simple example. For this case, [`JoinSet`] should probably be used instead.
63///
64/// ```
65/// use tokio_util::task::TaskTracker;
66///
67/// #[tokio::main]
68/// async fn main() {
69///     let tracker = TaskTracker::new();
70///
71///     for i in 0..10 {
72///         tracker.spawn(async move {
73///             println!("Task {} is running!", i);
74///         });
75///     }
76///     // Once we spawned everything, we close the tracker.
77///     tracker.close();
78///
79///     // Wait for everything to finish.
80///     tracker.wait().await;
81///
82///     println!("This is printed after all of the tasks.");
83/// }
84/// ```
85///
86/// ## Wait for tasks to exit
87///
88/// This example shows the intended use-case of `TaskTracker`. It is used together with
89/// [`CancellationToken`] to implement graceful shutdown.
90/// ```
91/// use tokio_util::sync::CancellationToken;
92/// use tokio_util::task::TaskTracker;
93/// use tokio::time::{self, Duration};
94///
95/// async fn background_task(num: u64) {
96///     for i in 0..10 {
97///         time::sleep(Duration::from_millis(100*num)).await;
98///         println!("Background task {} in iteration {}.", num, i);
99///     }
100/// }
101///
102/// #[tokio::main]
103/// # async fn _hidden() {}
104/// # #[tokio::main(flavor = "current_thread", start_paused = true)]
105/// async fn main() {
106///     let tracker = TaskTracker::new();
107///     let token = CancellationToken::new();
108///
109///     for i in 0..10 {
110///         let token = token.clone();
111///         tracker.spawn(async move {
112///             // Use a `tokio::select!` to kill the background task if the token is
113///             // cancelled.
114///             tokio::select! {
115///                 () = background_task(i) => {
116///                     println!("Task {} exiting normally.", i);
117///                 },
118///                 () = token.cancelled() => {
119///                     // Do some cleanup before we really exit.
120///                     time::sleep(Duration::from_millis(50)).await;
121///                     println!("Task {} finished cleanup.", i);
122///                 },
123///             }
124///         });
125///     }
126///
127///     // Spawn a background task that will send the shutdown signal.
128///     {
129///         let tracker = tracker.clone();
130///         tokio::spawn(async move {
131///             // Normally you would use something like ctrl-c instead of
132///             // sleeping.
133///             time::sleep(Duration::from_secs(2)).await;
134///             tracker.close();
135///             token.cancel();
136///         });
137///     }
138///
139///     // Wait for all tasks to exit.
140///     tracker.wait().await;
141///
142///     println!("All tasks have exited now.");
143/// }
144/// ```
145///
146/// [`CancellationToken`]: crate::sync::CancellationToken
147/// [`JoinHandle::is_finished`]: tokio::task::JoinHandle::is_finished
148/// [`JoinSet`]: tokio::task::JoinSet
149/// [`close`]: Self::close
150/// [`join_next`]: tokio::task::JoinSet::join_next
151/// [`wait`]: Self::wait
152/// [graceful shutdown]: https://tokio.rs/tokio/topics/shutdown
153pub struct TaskTracker {
154    inner: Arc<TaskTrackerInner>,
155}
156
157/// Represents a task tracked by a [`TaskTracker`].
158#[must_use]
159#[derive(Debug)]
160pub struct TaskTrackerToken {
161    task_tracker: TaskTracker,
162}
163
164struct TaskTrackerInner {
165    /// Keeps track of the state.
166    ///
167    /// The lowest bit is whether the task tracker is closed.
168    ///
169    /// The rest of the bits count the number of tracked tasks.
170    state: AtomicUsize,
171    /// Used to notify when the last task exits.
172    on_last_exit: Notify,
173}
174
175pin_project! {
176    /// A future that is tracked as a task by a [`TaskTracker`].
177    ///
178    /// The associated [`TaskTracker`] cannot complete until this future is dropped.
179    ///
180    /// This future is returned by [`TaskTracker::track_future`].
181    #[must_use = "futures do nothing unless polled"]
182    pub struct TrackedFuture<F> {
183        #[pin]
184        future: F,
185        token: TaskTrackerToken,
186    }
187}
188
189pin_project! {
190    /// A future that completes when the [`TaskTracker`] is empty and closed.
191    ///
192    /// This future is returned by [`TaskTracker::wait`].
193    #[must_use = "futures do nothing unless polled"]
194    pub struct TaskTrackerWaitFuture<'a> {
195        #[pin]
196        future: Notified<'a>,
197        inner: Option<&'a TaskTrackerInner>,
198    }
199}
200
201impl TaskTrackerInner {
202    #[inline]
203    fn new() -> Self {
204        Self {
205            state: AtomicUsize::new(0),
206            on_last_exit: Notify::new(),
207        }
208    }
209
210    #[inline]
211    fn is_closed_and_empty(&self) -> bool {
212        // If empty and closed bit set, then we are done.
213        //
214        // The acquire load will synchronize with the release store of any previous call to
215        // `set_closed` and `drop_task`.
216        self.state.load(Ordering::Acquire) == 1
217    }
218
219    #[inline]
220    fn set_closed(&self) -> bool {
221        // The AcqRel ordering makes the closed bit behave like a `Mutex<bool>` for synchronization
222        // purposes. We do this because it makes the return value of `TaskTracker::{close,reopen}`
223        // more meaningful for the user. Without these orderings, this assert could fail:
224        // ```
225        // // thread 1
226        // some_other_atomic.store(true, Relaxed);
227        // tracker.close();
228        //
229        // // thread 2
230        // if tracker.reopen() {
231        //     assert!(some_other_atomic.load(Relaxed));
232        // }
233        // ```
234        // However, with the AcqRel ordering, we establish a happens-before relationship from the
235        // call to `close` and the later call to `reopen` that returned true.
236        let state = self.state.fetch_or(1, Ordering::AcqRel);
237
238        // If there are no tasks, and if it was not already closed:
239        if state == 0 {
240            self.notify_now();
241        }
242
243        (state & 1) == 0
244    }
245
246    #[inline]
247    fn set_open(&self) -> bool {
248        // See `set_closed` regarding the AcqRel ordering.
249        let state = self.state.fetch_and(!1, Ordering::AcqRel);
250        (state & 1) == 1
251    }
252
253    #[inline]
254    fn add_task(&self) {
255        self.state.fetch_add(2, Ordering::Relaxed);
256    }
257
258    #[inline]
259    fn drop_task(&self) {
260        let state = self.state.fetch_sub(2, Ordering::Release);
261
262        // If this was the last task and we are closed:
263        if state == 3 {
264            self.notify_now();
265        }
266    }
267
268    #[cold]
269    fn notify_now(&self) {
270        // Insert an acquire fence. This matters for `drop_task` but doesn't matter for
271        // `set_closed` since it already uses AcqRel.
272        //
273        // This synchronizes with the release store of any other call to `drop_task`, and with the
274        // release store in the call to `set_closed`. That ensures that everything that happened
275        // before those other calls to `drop_task` or `set_closed` will be visible after this load,
276        // and those things will also be visible to anything woken by the call to `notify_waiters`.
277        self.state.load(Ordering::Acquire);
278
279        self.on_last_exit.notify_waiters();
280    }
281}
282
283impl TaskTracker {
284    /// Creates a new `TaskTracker`.
285    ///
286    /// The `TaskTracker` will start out as open.
287    #[must_use]
288    pub fn new() -> Self {
289        Self {
290            inner: Arc::new(TaskTrackerInner::new()),
291        }
292    }
293
294    /// Waits until this `TaskTracker` is both closed and empty.
295    ///
296    /// If the `TaskTracker` is already closed and empty when this method is called, then it
297    /// returns immediately.
298    ///
299    /// The `wait` future is resistant against [ABA problems][aba]. That is, if the `TaskTracker`
300    /// becomes both closed and empty for a short amount of time, then it is guarantee that all
301    /// `wait` futures that were created before the short time interval will trigger, even if they
302    /// are not polled during that short time interval.
303    ///
304    /// # Cancel safety
305    ///
306    /// This method is cancel safe.
307    ///
308    /// However, the resistance against [ABA problems][aba] is lost when using `wait` as the
309    /// condition in a `tokio::select!` loop.
310    ///
311    /// [aba]: https://en.wikipedia.org/wiki/ABA_problem
312    #[inline]
313    pub fn wait(&self) -> TaskTrackerWaitFuture<'_> {
314        TaskTrackerWaitFuture {
315            future: self.inner.on_last_exit.notified(),
316            inner: if self.inner.is_closed_and_empty() {
317                None
318            } else {
319                Some(&self.inner)
320            },
321        }
322    }
323
324    /// Close this `TaskTracker`.
325    ///
326    /// This allows [`wait`] futures to complete. It does not prevent you from spawning new tasks.
327    ///
328    /// Returns `true` if this closed the `TaskTracker`, or `false` if it was already closed.
329    ///
330    /// [`wait`]: Self::wait
331    #[inline]
332    pub fn close(&self) -> bool {
333        self.inner.set_closed()
334    }
335
336    /// Reopen this `TaskTracker`.
337    ///
338    /// This prevents [`wait`] futures from completing even if the `TaskTracker` is empty.
339    ///
340    /// Returns `true` if this reopened the `TaskTracker`, or `false` if it was already open.
341    ///
342    /// [`wait`]: Self::wait
343    #[inline]
344    pub fn reopen(&self) -> bool {
345        self.inner.set_open()
346    }
347
348    /// Returns `true` if this `TaskTracker` is [closed](Self::close).
349    #[inline]
350    #[must_use]
351    pub fn is_closed(&self) -> bool {
352        (self.inner.state.load(Ordering::Acquire) & 1) != 0
353    }
354
355    /// Returns the number of tasks tracked by this `TaskTracker`.
356    #[inline]
357    #[must_use]
358    pub fn len(&self) -> usize {
359        self.inner.state.load(Ordering::Acquire) >> 1
360    }
361
362    /// Returns `true` if there are no tasks in this `TaskTracker`.
363    #[inline]
364    #[must_use]
365    pub fn is_empty(&self) -> bool {
366        self.inner.state.load(Ordering::Acquire) <= 1
367    }
368
369    /// Spawn the provided future on the current Tokio runtime, and track it in this `TaskTracker`.
370    ///
371    /// This is equivalent to `tokio::spawn(tracker.track_future(task))`.
372    #[inline]
373    #[track_caller]
374    #[cfg(feature = "rt")]
375    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
376    pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
377    where
378        F: Future + Send + 'static,
379        F::Output: Send + 'static,
380    {
381        tokio::task::spawn(self.track_future(task))
382    }
383
384    /// Spawn the provided future on the provided Tokio runtime, and track it in this `TaskTracker`.
385    ///
386    /// This is equivalent to `handle.spawn(tracker.track_future(task))`.
387    #[inline]
388    #[track_caller]
389    #[cfg(feature = "rt")]
390    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
391    pub fn spawn_on<F>(&self, task: F, handle: &Handle) -> JoinHandle<F::Output>
392    where
393        F: Future + Send + 'static,
394        F::Output: Send + 'static,
395    {
396        handle.spawn(self.track_future(task))
397    }
398
399    /// Spawn the provided future on the current [`LocalSet`], and track it in this `TaskTracker`.
400    ///
401    /// This is equivalent to `tokio::task::spawn_local(tracker.track_future(task))`.
402    ///
403    /// [`LocalSet`]: tokio::task::LocalSet
404    #[inline]
405    #[track_caller]
406    #[cfg(feature = "rt")]
407    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
408    pub fn spawn_local<F>(&self, task: F) -> JoinHandle<F::Output>
409    where
410        F: Future + 'static,
411        F::Output: 'static,
412    {
413        tokio::task::spawn_local(self.track_future(task))
414    }
415
416    /// Spawn the provided future on the provided [`LocalSet`], and track it in this `TaskTracker`.
417    ///
418    /// This is equivalent to `local_set.spawn_local(tracker.track_future(task))`.
419    ///
420    /// [`LocalSet`]: tokio::task::LocalSet
421    #[inline]
422    #[track_caller]
423    #[cfg(feature = "rt")]
424    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
425    pub fn spawn_local_on<F>(&self, task: F, local_set: &LocalSet) -> JoinHandle<F::Output>
426    where
427        F: Future + 'static,
428        F::Output: 'static,
429    {
430        local_set.spawn_local(self.track_future(task))
431    }
432
433    /// Spawn the provided blocking task on the current Tokio runtime, and track it in this `TaskTracker`.
434    ///
435    /// This is equivalent to `tokio::task::spawn_blocking(tracker.track_future(task))`.
436    #[inline]
437    #[track_caller]
438    #[cfg(feature = "rt")]
439    #[cfg(not(target_family = "wasm"))]
440    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
441    pub fn spawn_blocking<F, T>(&self, task: F) -> JoinHandle<T>
442    where
443        F: FnOnce() -> T,
444        F: Send + 'static,
445        T: Send + 'static,
446    {
447        let token = self.token();
448        tokio::task::spawn_blocking(move || {
449            let res = task();
450            drop(token);
451            res
452        })
453    }
454
455    /// Spawn the provided blocking task on the provided Tokio runtime, and track it in this `TaskTracker`.
456    ///
457    /// This is equivalent to `handle.spawn_blocking(tracker.track_future(task))`.
458    #[inline]
459    #[track_caller]
460    #[cfg(feature = "rt")]
461    #[cfg(not(target_family = "wasm"))]
462    #[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
463    pub fn spawn_blocking_on<F, T>(&self, task: F, handle: &Handle) -> JoinHandle<T>
464    where
465        F: FnOnce() -> T,
466        F: Send + 'static,
467        T: Send + 'static,
468    {
469        let token = self.token();
470        handle.spawn_blocking(move || {
471            let res = task();
472            drop(token);
473            res
474        })
475    }
476
477    /// Track the provided future.
478    ///
479    /// The returned [`TrackedFuture`] will count as a task tracked by this collection, and will
480    /// prevent calls to [`wait`] from returning until the task is dropped.
481    ///
482    /// The task is removed from the collection when it is dropped, not when [`poll`] returns
483    /// [`Poll::Ready`].
484    ///
485    /// # Examples
486    ///
487    /// Track a future spawned with [`tokio::spawn`].
488    ///
489    /// ```
490    /// # async fn my_async_fn() {}
491    /// use tokio_util::task::TaskTracker;
492    ///
493    /// # #[tokio::main(flavor = "current_thread")]
494    /// # async fn main() {
495    /// let tracker = TaskTracker::new();
496    ///
497    /// tokio::spawn(tracker.track_future(my_async_fn()));
498    /// # }
499    /// ```
500    ///
501    /// Track a future spawned on a [`JoinSet`].
502    /// ```
503    /// # async fn my_async_fn() {}
504    /// use tokio::task::JoinSet;
505    /// use tokio_util::task::TaskTracker;
506    ///
507    /// # #[tokio::main(flavor = "current_thread")]
508    /// # async fn main() {
509    /// let tracker = TaskTracker::new();
510    /// let mut join_set = JoinSet::new();
511    ///
512    /// join_set.spawn(tracker.track_future(my_async_fn()));
513    /// # }
514    /// ```
515    ///
516    /// [`JoinSet`]: tokio::task::JoinSet
517    /// [`Poll::Pending`]: std::task::Poll::Pending
518    /// [`poll`]: std::future::Future::poll
519    /// [`wait`]: Self::wait
520    #[inline]
521    pub fn track_future<F: Future>(&self, future: F) -> TrackedFuture<F> {
522        TrackedFuture {
523            future,
524            token: self.token(),
525        }
526    }
527
528    /// Creates a [`TaskTrackerToken`] representing a task tracked by this `TaskTracker`.
529    ///
530    /// This token is a lower-level utility than the spawn methods. Each token is considered to
531    /// correspond to a task. As long as the token exists, the `TaskTracker` cannot complete.
532    /// Furthermore, the count returned by the [`len`] method will include the tokens in the count.
533    ///
534    /// Dropping the token indicates to the `TaskTracker` that the task has exited.
535    ///
536    /// [`len`]: TaskTracker::len
537    #[inline]
538    pub fn token(&self) -> TaskTrackerToken {
539        self.inner.add_task();
540        TaskTrackerToken {
541            task_tracker: self.clone(),
542        }
543    }
544
545    /// Returns `true` if both task trackers correspond to the same set of tasks.
546    ///
547    /// # Examples
548    ///
549    /// ```
550    /// use tokio_util::task::TaskTracker;
551    ///
552    /// let tracker_1 = TaskTracker::new();
553    /// let tracker_2 = TaskTracker::new();
554    /// let tracker_1_clone = tracker_1.clone();
555    ///
556    /// assert!(TaskTracker::ptr_eq(&tracker_1, &tracker_1_clone));
557    /// assert!(!TaskTracker::ptr_eq(&tracker_1, &tracker_2));
558    /// ```
559    #[inline]
560    #[must_use]
561    pub fn ptr_eq(left: &TaskTracker, right: &TaskTracker) -> bool {
562        Arc::ptr_eq(&left.inner, &right.inner)
563    }
564}
565
566impl Default for TaskTracker {
567    /// Creates a new `TaskTracker`.
568    ///
569    /// The `TaskTracker` will start out as open.
570    #[inline]
571    fn default() -> TaskTracker {
572        TaskTracker::new()
573    }
574}
575
576impl Clone for TaskTracker {
577    /// Returns a new `TaskTracker` that tracks the same set of tasks.
578    ///
579    /// Since the new `TaskTracker` shares the same set of tasks, changes to one set are visible in
580    /// all other clones.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use tokio_util::task::TaskTracker;
586    ///
587    /// #[tokio::main]
588    /// # async fn _hidden() {}
589    /// # #[tokio::main(flavor = "current_thread")]
590    /// async fn main() {
591    ///     let tracker = TaskTracker::new();
592    ///     let cloned = tracker.clone();
593    ///
594    ///     // Spawns on `tracker` are visible in `cloned`.
595    ///     tracker.spawn(std::future::pending::<()>());
596    ///     assert_eq!(cloned.len(), 1);
597    ///
598    ///     // Spawns on `cloned` are visible in `tracker`.
599    ///     cloned.spawn(std::future::pending::<()>());
600    ///     assert_eq!(tracker.len(), 2);
601    ///
602    ///     // Calling `close` is visible to `cloned`.
603    ///     tracker.close();
604    ///     assert!(cloned.is_closed());
605    ///
606    ///     // Calling `reopen` is visible to `tracker`.
607    ///     cloned.reopen();
608    ///     assert!(!tracker.is_closed());
609    /// }
610    /// ```
611    #[inline]
612    fn clone(&self) -> TaskTracker {
613        Self {
614            inner: self.inner.clone(),
615        }
616    }
617}
618
619fn debug_inner(inner: &TaskTrackerInner, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620    let state = inner.state.load(Ordering::Acquire);
621    let is_closed = (state & 1) != 0;
622    let len = state >> 1;
623
624    f.debug_struct("TaskTracker")
625        .field("len", &len)
626        .field("is_closed", &is_closed)
627        .field("inner", &(inner as *const TaskTrackerInner))
628        .finish()
629}
630
631impl fmt::Debug for TaskTracker {
632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633        debug_inner(&self.inner, f)
634    }
635}
636
637impl TaskTrackerToken {
638    /// Returns the [`TaskTracker`] that this token is associated with.
639    #[inline]
640    #[must_use]
641    pub fn task_tracker(&self) -> &TaskTracker {
642        &self.task_tracker
643    }
644}
645
646impl Clone for TaskTrackerToken {
647    /// Returns a new `TaskTrackerToken` associated with the same [`TaskTracker`].
648    ///
649    /// This is equivalent to `token.task_tracker().token()`.
650    #[inline]
651    fn clone(&self) -> TaskTrackerToken {
652        self.task_tracker.token()
653    }
654}
655
656impl Drop for TaskTrackerToken {
657    /// Dropping the token indicates to the [`TaskTracker`] that the task has exited.
658    #[inline]
659    fn drop(&mut self) {
660        self.task_tracker.inner.drop_task();
661    }
662}
663
664impl<F: Future> Future for TrackedFuture<F> {
665    type Output = F::Output;
666
667    #[inline]
668    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
669        self.project().future.poll(cx)
670    }
671}
672
673impl<F: fmt::Debug> fmt::Debug for TrackedFuture<F> {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675        f.debug_struct("TrackedFuture")
676            .field("future", &self.future)
677            .field("task_tracker", self.token.task_tracker())
678            .finish()
679    }
680}
681
682impl<'a> Future for TaskTrackerWaitFuture<'a> {
683    type Output = ();
684
685    #[inline]
686    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
687        let me = self.project();
688
689        let inner = match me.inner.as_ref() {
690            None => return Poll::Ready(()),
691            Some(inner) => inner,
692        };
693
694        let ready = inner.is_closed_and_empty() || me.future.poll(cx).is_ready();
695        if ready {
696            *me.inner = None;
697            Poll::Ready(())
698        } else {
699            Poll::Pending
700        }
701    }
702}
703
704impl<'a> fmt::Debug for TaskTrackerWaitFuture<'a> {
705    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
706        struct Helper<'a>(&'a TaskTrackerInner);
707
708        impl fmt::Debug for Helper<'_> {
709            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710                debug_inner(self.0, f)
711            }
712        }
713
714        f.debug_struct("TaskTrackerWaitFuture")
715            .field("future", &self.future)
716            .field("task_tracker", &self.inner.map(Helper))
717            .finish()
718    }
719}