umsh_sync/
async_condition.rs

1//! Single-threaded, no_std + alloc port of the `AsyncCondition` primitive from
2//! rust-lumanoi-core (originally under the Fuchsia BSD-style license).
3//!
4//! Upstream source:
5//! `/Users/darco/Projects/rust-lumanoi/rust-lumanoi-core/src/async_condition.rs`
6//!
7//! Differences from the upstream:
8//! - `std::sync::Mutex` → `core::cell::RefCell` (UMSH is single-threaded).
9//! - `AtomicUsize` trigger counter → `Cell<usize>`.
10//!
11//! Everything else (`Slab<Waker>`, ticket-based deregistration, `forget_ticket`,
12//! the counter-snapshot check in `poll_wait` that prevents lost wakes across
13//! trigger-then-wait races) is preserved.
14//
15// Copyright 2020 The Fuchsia Authors. All rights reserved.
16// Use of this source code is governed by a BSD-style license that can be
17// found in the LICENSE file of the upstream rust-lumanoi repository.
18
19use core::cell::{Cell, RefCell};
20use core::future::Future;
21use core::pin::Pin;
22use core::task::{Context, Poll, Waker};
23
24use slab::Slab;
25
26/// An asynchronous condition that can block multiple tasks until triggered.
27#[derive(Debug)]
28pub struct AsyncCondition {
29    wakers: RefCell<Slab<Waker>>,
30    trigger_counter: Cell<usize>,
31}
32
33impl Default for AsyncCondition {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl AsyncCondition {
40    pub fn new() -> Self {
41        AsyncCondition {
42            wakers: RefCell::new(Slab::with_capacity(4)),
43            trigger_counter: Cell::new(1),
44        }
45    }
46
47    /// Returns a future that will block until `trigger()` is next called.
48    pub fn wait(&self) -> AsyncConditionWait<'_> {
49        AsyncConditionWait {
50            condition: self,
51            ticket: self.ticket(),
52        }
53    }
54
55    pub fn ticket(&self) -> AsyncConditionTicket {
56        AsyncConditionTicket {
57            key: None,
58            trigger_after: self.trigger_counter.get(),
59        }
60    }
61
62    /// Wakes all pending `AsyncConditionWait` instances vended by `wait()`.
63    pub fn trigger(&self) {
64        let wakers = self.wakers.borrow();
65        self.trigger_counter
66            .set(self.trigger_counter.get().wrapping_add(1));
67        for (_, waker) in wakers.iter() {
68            waker.wake_by_ref();
69        }
70    }
71
72    pub fn is_ticket_triggered(&self, ticket: &AsyncConditionTicket) -> bool {
73        ticket.is_terminated() || ticket.trigger_after != self.trigger_counter.get()
74    }
75
76    pub fn forget_ticket(&self, ticket: &mut AsyncConditionTicket) {
77        if let Some(key) = ticket.key.take() {
78            let mut wakers = self.wakers.borrow_mut();
79            assert!(
80                wakers.contains(key),
81                "AsyncConditionTicket contained invalid waker key"
82            );
83            wakers.remove(key);
84        }
85        ticket.trigger_after = 0;
86    }
87
88    pub fn poll_wait(
89        &self,
90        context: &mut Context<'_>,
91        ticket: &mut AsyncConditionTicket,
92    ) -> Poll<()> {
93        if self.is_ticket_triggered(ticket) {
94            ticket.trigger_after = 0;
95            return Poll::Ready(());
96        }
97        let mut wakers = self.wakers.borrow_mut();
98        if let Some(slot) = ticket.key.and_then(|k| wakers.get_mut(k)) {
99            *slot = context.waker().clone();
100        } else {
101            ticket.key = Some(wakers.insert(context.waker().clone()));
102        }
103        Poll::Pending
104    }
105}
106
107#[derive(Debug, Default)]
108pub struct AsyncConditionTicket {
109    key: Option<usize>,
110    trigger_after: usize,
111}
112
113impl AsyncConditionTicket {
114    pub fn is_terminated(&self) -> bool {
115        self.trigger_after == 0
116    }
117}
118
119/// Instance of `Future` returned by `AsyncCondition.wait()`.
120#[must_use = "futures do nothing unless polled"]
121#[derive(Debug)]
122pub struct AsyncConditionWait<'a> {
123    condition: &'a AsyncCondition,
124    ticket: AsyncConditionTicket,
125}
126
127impl<'a> AsyncConditionWait<'a> {
128    pub fn is_triggered(&self) -> bool {
129        self.condition.is_ticket_triggered(&self.ticket)
130    }
131
132    pub fn is_terminated(&self) -> bool {
133        self.ticket.is_terminated()
134    }
135}
136
137impl<'a> Future for AsyncConditionWait<'a> {
138    type Output = ();
139    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
140        let this = &mut *self;
141        this.condition.poll_wait(context, &mut this.ticket)
142    }
143}
144
145impl<'a> Drop for AsyncConditionWait<'a> {
146    fn drop(&mut self) {
147        self.condition.forget_ticket(&mut self.ticket);
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use futures::future::{Either, FutureExt, pending, ready, select};
155
156    /// Straight port of the upstream `test_async_condition` test, adapted
157    /// to `futures::executor::block_on` since we don't have `fuchsia_async`.
158    #[test]
159    fn test_async_condition() {
160        let condition = AsyncCondition::new();
161        assert_eq!(condition.wait().now_or_never(), None);
162        let waiter = condition.wait();
163        condition.trigger();
164        assert_eq!(waiter.now_or_never(), Some(()));
165
166        // Join a waiter with a trigger — the trigger resolves the waiter.
167        futures::executor::block_on(async {
168            let waiter = condition.wait();
169            futures::join!(waiter, async {
170                condition.trigger();
171            });
172        });
173    }
174
175    /// Ported from upstream `test_cancel_upon`, rewritten to use `select`
176    /// directly so we don't have to port the `FutureExt::cancel_upon` helper.
177    #[test]
178    fn test_cancel_upon_equivalent() {
179        let condition = AsyncCondition::new();
180
181        // Pending future + un-triggered condition: neither side resolves.
182        let fut = select(pending::<()>(), condition.wait());
183        assert!(fut.now_or_never().is_none());
184
185        // Trigger, then the condition-side wins and the combined future resolves.
186        let fut = select(pending::<()>(), condition.wait());
187        condition.trigger();
188        let resolved = fut
189            .map(|e| match e {
190                Either::Left(((), _)) => 1,
191                Either::Right(((), _)) => 2,
192            })
193            .now_or_never();
194        assert_eq!(resolved, Some(2));
195        let _ = ready::<()>(()); // touch `ready` import to avoid unused-import lint
196    }
197
198    /// Dropping one waiter must not stop a later trigger from waking a
199    /// different waiter. This exercises the slab's per-ticket deregistration.
200    #[test]
201    fn dropping_one_waiter_does_not_disturb_others() {
202        futures::executor::block_on(async {
203            let condition = AsyncCondition::new();
204            let wait_a = condition.wait();
205            let wait_b = condition.wait();
206
207            drop(wait_a);
208
209            // Trigger after drop; wait_b must still resolve.
210            futures::join!(wait_b, async { condition.trigger() });
211        });
212    }
213
214    /// `forget_ticket` terminates a ticket even before it's polled.
215    #[test]
216    fn forget_ticket_terminates() {
217        let condition = AsyncCondition::new();
218        let mut tkt = condition.ticket();
219        assert!(!tkt.is_terminated());
220        condition.forget_ticket(&mut tkt);
221        assert!(tkt.is_terminated());
222    }
223}