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}