Skip to main content

tor_async_utils/
oneshot_broadcast.rs

1//! A oneshot broadcast channel.
2//!
3//! The motivation for this channel type was to allow multiple
4//! receivers to either wait for something to finish,
5//! or to have an inexpensive method of checking if it has finished.
6//!
7//! See [`channel()`].
8
9use std::future::{Future, IntoFuture};
10use std::ops::Drop;
11use std::pin::Pin;
12use std::sync::{Arc, Mutex, OnceLock, Weak};
13use std::task::{Context, Poll, Waker, ready};
14
15use slotmap_careful::DenseSlotMap;
16
17slotmap_careful::new_key_type! { struct WakerKey; }
18
19/// A [oneshot broadcast][crate::oneshot_broadcast] sender.
20#[derive(Debug)]
21pub struct Sender<T> {
22    /// State shared with all [`Receiver`]s.
23    shared: Weak<Shared<T>>,
24}
25
26/// A [oneshot broadcast][crate::oneshot_broadcast] receiver.
27///
28/// The `Receiver` offers two methods for receiving the message:
29///
30/// 1. [`Receiver::into_future`]
31///     ```rust
32///     # use tor_async_utils::oneshot_broadcast::{channel, SenderDropped};
33///     # async fn x() -> Result<(), SenderDropped> {
34///     let (tx, rx) = channel();
35///     tx.send(0);
36///     let message: u32 = rx.await.unwrap();
37///     # Ok(())
38///     # }
39///     ```
40///
41/// 2. [`Receiver::borrowed`]
42///     ```rust
43///     # use tor_async_utils::oneshot_broadcast::{channel, SenderDropped};
44///     # async fn x() -> Result<(), SenderDropped> {
45///     let (tx, rx) = channel();
46///     tx.send(0);
47///     let message: &u32 = rx.borrowed().await.unwrap();
48///     # Ok(())
49///     # }
50///     ```
51#[derive(Clone, Debug)]
52pub struct Receiver<T> {
53    /// State shared with the sender and all other receivers.
54    shared: Arc<Shared<T>>,
55}
56
57/// State shared between the sender and receivers.
58/// Correctness:
59///
60/// Sending a message:
61///  - set the message OnceLock (A)
62///  - acquire the wakers Mutex
63///  - take all wakers (B)
64///  - release the wakers Mutex (C)
65///  - wake all wakers
66///
67/// Polling:
68///  - if message was set, return it (fast path)
69///  - acquire the wakers Mutex (D)
70///  - if message was set, return it (E)
71///  - add waker (F)
72///  - release the wakers Mutex
73///
74/// When the wakers Mutex is released at (C), a release-store operation is performed by the Mutex,
75/// which means that the message set at (A) will be seen by all future acquire-load operations by
76/// that same Mutex. More specifically, after (C) has occurred and when the same mutex is acquired at
77/// (D), the message set at (A) is guaranteed to be visible at (E). This means that after the wakers
78/// are taken at (B), no future wakers will be added at (F) and no waker will be "lost".
79#[derive(Debug)]
80struct Shared<T> {
81    /// The message sent from the [`Sender`] to the [`Receiver`]s.
82    msg: OnceLock<Result<T, SenderDropped>>,
83    /// The wakers waiting for a value to be sent.
84    /// Will be set to `Err` after the wakers have been woken.
85    // the `Result` isn't technically needed here,
86    // but we use it to help detect bugs;
87    // see `WakersAlreadyWoken` for details
88    wakers: Mutex<Result<DenseSlotMap<WakerKey, Waker>, WakersAlreadyWoken>>,
89}
90
91/// The future from [`Receiver::borrowed`].
92///
93/// Will be ready, yielding `&'a T`,
94/// when the sender sends a message or is dropped.
95#[derive(Debug)]
96pub struct BorrowedReceiverFuture<'a, T> {
97    /// State shared with the sender and all other receivers.
98    shared: &'a Shared<T>,
99    /// The key for any waker that we've added to [`Shared::wakers`].
100    waker_key: Option<WakerKey>,
101}
102
103/// The future from [`Receiver::into_future`].
104///
105/// Will be ready, yielding a clone of `T`,
106/// when the sender sends a message or is dropped.
107// Both `ReceiverFuture` and `BorrowedReceiverFuture` have similar fields
108// but there's no nice way to deduplicated them.
109// It would have been nice if we could store a `BorrowedReceiverFuture`
110// holding a reference to our `Arc<Shared>`,
111// but that would be a self-referential struct,
112// so we need to duplicate the fields here instead.
113#[derive(Debug)]
114pub struct ReceiverFuture<T> {
115    /// State shared with the sender and all other receivers.
116    shared: Arc<Shared<T>>,
117    /// The key for any waker that we've added to [`Shared::wakers`].
118    waker_key: Option<WakerKey>,
119}
120
121/// The wakers have already been woken.
122///
123/// This is used to help detect if we're trying to access the wakers after they've already been
124/// woken, which likely indicates a bug. For example, it is a bug if a receiver attempts to add a
125/// waker after the sender has already sent its message and woken the wakers, since the new waker
126/// would never be woken.
127#[derive(Copy, Clone, Debug)]
128struct WakersAlreadyWoken;
129
130/// The message has already been set, and we can't set it again.
131#[derive(Copy, Clone, Debug, thiserror::Error)]
132#[error("the message was already set")]
133struct MessageAlreadySet;
134
135/// The sender was dropped, so the channel is closed.
136#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
137#[error("the sender was dropped")]
138#[allow(clippy::exhaustive_structs)]
139pub struct SenderDropped;
140
141/// Create a new oneshot broadcast channel.
142///
143/// ```rust
144/// # use tor_async_utils::oneshot_broadcast::{channel, SenderDropped};
145/// # async fn x() -> Result<(), SenderDropped> {
146/// let (tx, rx) = channel();
147/// let rx_clone = rx.clone();
148/// tx.send(0_u8);
149/// assert_eq!(rx.await, Ok(0));
150/// assert_eq!(rx_clone.await, Ok(0));
151/// # Ok(())
152/// # }
153/// ```
154pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
155    let shared = Arc::new(Shared {
156        msg: OnceLock::new(),
157        wakers: Mutex::new(Ok(DenseSlotMap::with_key())),
158    });
159
160    let sender = Sender {
161        shared: Arc::downgrade(&shared),
162    };
163
164    let receiver = Receiver { shared };
165
166    (sender, receiver)
167}
168
169impl<T> Sender<T> {
170    /// Send the message to the [`Receiver`]s.
171    ///
172    /// The message may be lost if all receivers have been dropped.
173    pub fn send(self, msg: T) {
174        // set the message and inform the wakers
175        Self::send_and_wake(&self.shared, Ok(msg))
176            // this 'send()` method takes an owned self,
177            // and we don't send a message outside of here and the drop handler,
178            // so this shouldn't be possible
179            .expect("could not set the message");
180    }
181
182    /// Send the message, and wake and clear all wakers.
183    ///
184    /// If all receivers have been dropped, then always returns `Ok`.
185    ///
186    /// If the message was unable to be set, returns `Err(MessageAlreadySet)`.
187    fn send_and_wake(
188        shared: &Weak<Shared<T>>,
189        msg: Result<T, SenderDropped>,
190    ) -> Result<(), MessageAlreadySet> {
191        // Even if the `Weak` upgrade is successful,
192        // it's possible that the last receiver
193        // will be dropped during this `send_and_wake` method,
194        // in which case we will be holding the last `Arc`.
195        let Some(shared) = shared.upgrade() else {
196            // all receivers have dropped; nothing to do
197            return Ok(());
198        };
199
200        // set the message
201        shared.msg.set(msg).or(Err(MessageAlreadySet))?;
202
203        let mut wakers = {
204            let mut wakers = shared.wakers.lock().expect("poisoned");
205            // Take the wakers and drop the mutex guard, releasing the lock.
206            //
207            // We could just drain the wakers map in-place here, but instead we replace the map with
208            // an explicit `WakersAlreadyWoken` state to help catch bugs if something tries adding a
209            // new waker later after we've already woken the wakers.
210            //
211            // The above `msg.set()` will only ever succeed once,
212            // which means that we should only end up here once.
213            std::mem::replace(&mut *wakers, Err(WakersAlreadyWoken))
214                .expect("wakers were taken more than once")
215        };
216
217        // Once we drop the mutex guard, which does a release-store on its own atomic, any other
218        // code which later acquires the wakers mutex is guaranteed to see the msg as "set".
219        // See comments on `Shared`.
220
221        // Wake while not holding the lock.
222        // Since the lock is used in `ReceiverFuture::poll` and `ReceiverFuture::drop` and
223        // should not block for long periods of time,
224        // we'd prefer not to run third-party waker code here while holding the mutex,
225        // even if `wake` should typically be fast.
226        for (_key, waker) in wakers.drain() {
227            waker.wake();
228        }
229
230        Ok(())
231    }
232
233    /// Returns `true` if all [`Receiver`]s (and all futures created from the receivers) have been
234    /// dropped.
235    ///
236    /// This can be useful to skip doing extra work to generate the message if the message will be
237    /// discarded anyways.
238    // This is for external use.
239    // It is not always valid to call this internally.
240    // For example when we've done a `Weak::upgrade` internally, like in `send_and_wake`,
241    // this won't return the correct value.
242    pub fn is_cancelled(&self) -> bool {
243        self.shared.strong_count() == 0
244    }
245}
246
247impl<T> Drop for Sender<T> {
248    fn drop(&mut self) {
249        // set an error message to indicate that the sender was dropped and inform the wakers;
250        // it's fine if setting the message fails since it might have been set previously during a
251        // `send()`
252        let _ = Self::send_and_wake(&self.shared, Err(SenderDropped));
253    }
254}
255
256impl<T> Receiver<T> {
257    /// Receive a borrowed message from the [`Sender`].
258    ///
259    /// This may be more efficient than [`Receiver::into_future`]
260    /// and doesn't require `T: Clone`.
261    ///
262    /// This is cancellation-safe.
263    pub fn borrowed(&self) -> BorrowedReceiverFuture<'_, T> {
264        BorrowedReceiverFuture {
265            shared: &self.shared,
266            waker_key: None,
267        }
268    }
269
270    /// The receiver is ready.
271    ///
272    /// If `true`, the [`Sender`] has either sent its message or been dropped.
273    pub fn is_ready(&self) -> bool {
274        self.shared.msg.get().is_some()
275    }
276}
277
278impl<T: Clone> IntoFuture for Receiver<T> {
279    type Output = Result<T, SenderDropped>;
280    type IntoFuture = ReceiverFuture<T>;
281
282    /// This future is cancellation-safe.
283    fn into_future(self) -> Self::IntoFuture {
284        ReceiverFuture {
285            shared: self.shared,
286            waker_key: None,
287        }
288    }
289}
290
291impl<'a, T> Future for BorrowedReceiverFuture<'a, T> {
292    type Output = Result<&'a T, SenderDropped>;
293
294    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
295        let self_ = self.get_mut();
296        receiver_fut_poll(self_.shared, &mut self_.waker_key, cx.waker())
297    }
298}
299
300impl<T> Drop for BorrowedReceiverFuture<'_, T> {
301    fn drop(&mut self) {
302        receiver_fut_drop(self.shared, &mut self.waker_key);
303    }
304}
305
306impl<T: Clone> Future for ReceiverFuture<T> {
307    type Output = Result<T, SenderDropped>;
308
309    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
310        let self_ = self.get_mut();
311        let poll = receiver_fut_poll(&self_.shared, &mut self_.waker_key, cx.waker());
312        Poll::Ready(ready!(poll)).map_ok(Clone::clone)
313    }
314}
315
316impl<T> Drop for ReceiverFuture<T> {
317    fn drop(&mut self) {
318        receiver_fut_drop(&self.shared, &mut self.waker_key);
319    }
320}
321
322/// The shared poll implementation for receiver futures.
323fn receiver_fut_poll<'a, T>(
324    shared: &'a Shared<T>,
325    waker_key: &mut Option<WakerKey>,
326    new_waker: &Waker,
327) -> Poll<Result<&'a T, SenderDropped>> {
328    // if the message was already set, return it
329    if let Some(msg) = shared.msg.get() {
330        return Poll::Ready(msg.as_ref().or(Err(SenderDropped)));
331    }
332
333    let mut wakers = shared.wakers.lock().expect("poisoned");
334
335    // check again now that we've acquired the mutex
336    if let Some(msg) = shared.msg.get() {
337        return Poll::Ready(msg.as_ref().or(Err(SenderDropped)));
338    }
339
340    // we have acquired the wakers mutex and checked that the message wasn't set,
341    // so we know that wakers have not yet been woken
342    // and it's okay to add our waker to the wakers map
343    let wakers = wakers.as_mut().expect("wakers were already woken");
344
345    match waker_key {
346        // we have added a waker previously
347        Some(waker_key) => {
348            // replace the old entry
349            let waker = wakers
350                .get_mut(*waker_key)
351                // the waker is only removed from the map by our drop handler,
352                // so the waker should never be missing
353                .expect("waker key is missing from map");
354            waker.clone_from(new_waker);
355        }
356        // we have never added a waker
357        None => {
358            // add a new entry
359            let new_key = wakers.insert(new_waker.clone());
360            *waker_key = Some(new_key);
361        }
362    }
363
364    Poll::Pending
365}
366
367/// The shared drop implementation for receiver futures.
368fn receiver_fut_drop<T>(shared: &Shared<T>, waker_key: &mut Option<WakerKey>) {
369    if let Some(waker_key) = waker_key.take() {
370        let mut wakers = shared.wakers.lock().expect("poisoned");
371        if let Ok(wakers) = wakers.as_mut() {
372            let waker = wakers.remove(waker_key);
373            // this is the only place that removes the waker from the map,
374            // so the waker should never be missing
375            debug_assert!(waker.is_some(), "the waker key was not found");
376        }
377    }
378}
379
380#[cfg(test)]
381mod test {
382    #![allow(clippy::unwrap_used)]
383
384    use super::*;
385
386    use futures::future::FutureExt;
387    use tor_rtcompat::SpawnExt;
388
389    impl<T> Shared<T> {
390        /// Count the number of wakers.
391        fn count_wakers(&self) -> usize {
392            self.wakers
393                .lock()
394                .expect("poisoned")
395                .as_ref()
396                .map(|x| x.len())
397                .unwrap_or(0)
398        }
399    }
400
401    #[test]
402    fn standard_usage() {
403        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
404            let (tx, rx) = channel();
405            tx.send(0_u8);
406            assert_eq!(rx.borrowed().await, Ok(&0));
407
408            let (tx, rx) = channel();
409            tx.send(0_u8);
410            assert_eq!(rx.await, Ok(0));
411        });
412    }
413
414    #[test]
415    fn immediate_drop() {
416        let _ = channel::<()>();
417
418        let (tx, rx) = channel::<()>();
419        drop(tx);
420        drop(rx);
421
422        let (tx, rx) = channel::<()>();
423        drop(rx);
424        drop(tx);
425    }
426
427    #[test]
428    fn drop_sender() {
429        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
430            let (tx, rx_1) = channel::<u8>();
431
432            let rx_2 = rx_1.clone();
433            drop(tx);
434            let rx_3 = rx_1.clone();
435            assert_eq!(rx_1.borrowed().await, Err(SenderDropped));
436            assert_eq!(rx_2.borrowed().await, Err(SenderDropped));
437            assert_eq!(rx_3.borrowed().await, Err(SenderDropped));
438        });
439    }
440
441    #[test]
442    fn clone_before_send() {
443        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
444            let (tx, rx_1) = channel();
445
446            let rx_2 = rx_1.clone();
447            tx.send(0_u8);
448            assert_eq!(rx_1.borrowed().await, Ok(&0));
449            assert_eq!(rx_2.borrowed().await, Ok(&0));
450        });
451    }
452
453    #[test]
454    fn clone_after_send() {
455        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
456            let (tx, rx_1) = channel();
457
458            tx.send(0_u8);
459            let rx_2 = rx_1.clone();
460            assert_eq!(rx_1.borrowed().await, Ok(&0));
461            assert_eq!(rx_2.borrowed().await, Ok(&0));
462        });
463    }
464
465    #[test]
466    fn clone_after_borrowed() {
467        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
468            let (tx, rx_1) = channel();
469
470            tx.send(0_u8);
471            assert_eq!(rx_1.borrowed().await, Ok(&0));
472            let rx_2 = rx_1.clone();
473            assert_eq!(rx_2.borrowed().await, Ok(&0));
474        });
475    }
476
477    #[test]
478    fn drop_one_receiver() {
479        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
480            let (tx, rx_1) = channel();
481
482            let rx_2 = rx_1.clone();
483            drop(rx_1);
484            tx.send(0_u8);
485            assert_eq!(rx_2.borrowed().await, Ok(&0));
486        });
487    }
488
489    #[test]
490    fn drop_all_receivers() {
491        let (tx, rx_1) = channel();
492
493        let rx_2 = rx_1.clone();
494        drop(rx_1);
495        drop(rx_2);
496        tx.send(0_u8);
497    }
498
499    #[test]
500    fn drop_fut() {
501        let (_tx, rx) = channel::<u8>();
502        let fut = rx.borrowed();
503        assert_eq!(rx.shared.count_wakers(), 0);
504        drop(fut);
505        assert_eq!(rx.shared.count_wakers(), 0);
506
507        // drop after sending
508        let (tx, rx) = channel();
509        tx.send(0_u8);
510        let fut = rx.borrowed();
511        assert_eq!(rx.shared.count_wakers(), 0);
512        drop(fut);
513        assert_eq!(rx.shared.count_wakers(), 0);
514
515        // drop after polling once
516        let (_tx, rx) = channel::<u8>();
517        let mut fut = Box::pin(rx.borrowed());
518        assert_eq!(rx.shared.count_wakers(), 0);
519        assert_eq!(fut.as_mut().now_or_never(), None);
520        assert_eq!(rx.shared.count_wakers(), 1);
521        drop(fut);
522        assert_eq!(rx.shared.count_wakers(), 0);
523
524        // drop after polling once and send
525        let (tx, rx) = channel();
526        let mut fut = Box::pin(rx.borrowed());
527        assert_eq!(rx.shared.count_wakers(), 0);
528        assert_eq!(fut.as_mut().now_or_never(), None);
529        assert_eq!(rx.shared.count_wakers(), 1);
530        tx.send(0_u8);
531        assert_eq!(rx.shared.count_wakers(), 0);
532        drop(fut);
533    }
534
535    #[test]
536    fn drop_owned_fut() {
537        let (_tx, rx) = channel::<u8>();
538        let fut = rx.clone().into_future();
539        assert_eq!(rx.shared.count_wakers(), 0);
540        drop(fut);
541        assert_eq!(rx.shared.count_wakers(), 0);
542
543        // drop after sending
544        let (tx, rx) = channel();
545        tx.send(0_u8);
546        let fut = rx.clone().into_future();
547        assert_eq!(rx.shared.count_wakers(), 0);
548        drop(fut);
549        assert_eq!(rx.shared.count_wakers(), 0);
550
551        // drop after polling once
552        let (_tx, rx) = channel::<u8>();
553        let mut fut = Box::pin(rx.clone().into_future());
554        assert_eq!(rx.shared.count_wakers(), 0);
555        assert_eq!(fut.as_mut().now_or_never(), None);
556        assert_eq!(rx.shared.count_wakers(), 1);
557        drop(fut);
558        assert_eq!(rx.shared.count_wakers(), 0);
559
560        // drop after polling once and send
561        let (tx, rx) = channel();
562        let mut fut = Box::pin(rx.clone().into_future());
563        assert_eq!(rx.shared.count_wakers(), 0);
564        assert_eq!(fut.as_mut().now_or_never(), None);
565        assert_eq!(rx.shared.count_wakers(), 1);
566        tx.send(0_u8);
567        assert_eq!(rx.shared.count_wakers(), 0);
568        drop(fut);
569    }
570
571    #[test]
572    fn is_ready_after_send() {
573        let (tx, rx_1) = channel();
574        assert!(!rx_1.is_ready());
575        let rx_2 = rx_1.clone();
576        assert!(!rx_2.is_ready());
577
578        tx.send(0_u8);
579
580        assert!(rx_1.is_ready());
581        assert!(rx_2.is_ready());
582
583        let rx_3 = rx_1.clone();
584        assert!(rx_3.is_ready());
585    }
586
587    #[test]
588    fn is_ready_after_drop() {
589        let (tx, rx_1) = channel::<u8>();
590        assert!(!rx_1.is_ready());
591        let rx_2 = rx_1.clone();
592        assert!(!rx_2.is_ready());
593
594        drop(tx);
595
596        assert!(rx_1.is_ready());
597        assert!(rx_2.is_ready());
598
599        let rx_3 = rx_1.clone();
600        assert!(rx_3.is_ready());
601    }
602
603    #[test]
604    fn is_cancelled() {
605        let (tx, rx) = channel::<u8>();
606        assert!(!tx.is_cancelled());
607        drop(rx);
608        assert!(tx.is_cancelled());
609
610        let (tx, rx_1) = channel::<u8>();
611        assert!(!tx.is_cancelled());
612        let rx_2 = rx_1.clone();
613        drop(rx_1);
614        assert!(!tx.is_cancelled());
615        drop(rx_2);
616        assert!(tx.is_cancelled());
617    }
618
619    #[test]
620    fn recv_in_task() {
621        tor_rtmock::MockRuntime::test_with_various(|rt| async move {
622            let (tx, rx) = channel();
623
624            let join = rt
625                .spawn_with_handle(async move {
626                    assert_eq!(rx.borrowed().await, Ok(&0));
627                    assert_eq!(rx.await, Ok(0));
628                })
629                .unwrap();
630
631            tx.send(0_u8);
632
633            join.await;
634        });
635    }
636
637    #[test]
638    fn recv_multiple_in_task() {
639        tor_rtmock::MockRuntime::test_with_various(|rt| async move {
640            let (tx, rx) = channel();
641            let rx_1 = rx.clone();
642            let rx_2 = rx.clone();
643
644            let join_1 = rt
645                .spawn_with_handle(async move {
646                    assert_eq!(rx_1.borrowed().await, Ok(&0));
647                })
648                .unwrap();
649            let join_2 = rt
650                .spawn_with_handle(async move {
651                    assert_eq!(rx_2.await, Ok(0));
652                })
653                .unwrap();
654
655            tx.send(0_u8);
656
657            join_1.await;
658            join_2.await;
659            assert_eq!(rx.borrowed().await, Ok(&0));
660        });
661    }
662
663    #[test]
664    fn recv_multiple_times() {
665        tor_rtmock::MockRuntime::test_with_various(|_rt| async move {
666            let (tx, rx) = channel();
667
668            tx.send(0_u8);
669            assert_eq!(rx.borrowed().await, Ok(&0));
670            assert_eq!(rx.borrowed().await, Ok(&0));
671            assert_eq!(rx.clone().await, Ok(0));
672            assert_eq!(rx.await, Ok(0));
673        });
674    }
675
676    #[test]
677    fn stress() {
678        // In general we don't have control over the runtime and where/when tasks are scheduled,
679        // so we try as best as possible to send the message while simultaneously creating new
680        // receivers and waiting on them.
681        // It's possible this might be entirely ineffective since we don't enforce any specific
682        // scheduler behaviour here,
683        // but in the worst case it's still a test with multiple receivers on different tasks,
684        // so is useful to have.
685        //
686        // The `test_with_various` helper uses `MockExecutor` with two different deterministic
687        // scheduling policies.
688        // At least at the time of writing,
689        // when this test uses `MockExecutor` with its "queue" scheduling policy
690        // the "send" occurs after 20 of the tasks have begun waiting.
691        tor_rtmock::MockRuntime::test_with_various(|rt| async move {
692            let (tx, rx) = channel();
693
694            rt.spawn(async move {
695                // this tries to delay the send a little bit
696                // to give time for some of the receiver tasks to start
697                for _ in 0..20 {
698                    tor_rtcompat::task::yield_now().await;
699                }
700                tx.send(0_u8);
701            })
702            .unwrap();
703
704            let mut joins = vec![];
705            for _ in 0..100 {
706                let rx_clone = rx.clone();
707                let join = rt
708                    .spawn_with_handle(async move { rx_clone.borrowed().await.cloned() })
709                    .unwrap();
710                joins.push(join);
711                // allows the send task to make progress if single-threaded
712                tor_rtcompat::task::yield_now().await;
713            }
714
715            for join in joins {
716                assert!(matches!(join.await, Ok(0)));
717            }
718        });
719    }
720}