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