Skip to main content

cranpose_services/
async_io.rs

1//! Waking a future from another thread.
2//!
3//! A platform's own I/O is blocking — a synchronous HTTP read, a provider call,
4//! a file descriptor a system API hands over — and the framework has no thread
5//! pool to hide that behind. What it has is the waker: work runs on a thread of
6//! its own and, when it has something, wakes whoever was awaiting it.
7//!
8//! Two shapes cover everything here. A [`Signal`] carries one value, which is
9//! what a request's status line is. A [`ChunkChannel`] carries a stream of byte
10//! chunks with the consumer's progress bounding the producer, which is what a
11//! response body is — without the bound, a slow reader and a fast server put the
12//! whole download in memory, which is the thing streaming exists to avoid.
13
14use std::{
15    collections::VecDeque,
16    future::Future,
17    pin::Pin,
18    sync::{Arc, Condvar, Mutex},
19    task::{Context, Poll, Waker},
20};
21
22/// How many chunks may wait ahead of the consumer before the producer stops
23/// reading. Enough to keep a socket busy across one scheduling gap, and far
24/// short of holding a download in memory.
25pub const MAX_PENDING_CHUNKS: usize = 8;
26
27struct SignalState<T> {
28    value: Option<T>,
29    waker: Option<Waker>,
30    closed: bool,
31}
32
33/// A one-value hand-off between a worker and a future.
34///
35/// The worker calls [`Signal::set`]; whoever awaits [`Signal::wait`] is woken
36/// with it. A worker that dies without setting anything closes the signal, and
37/// the wait resolves to `None` rather than hanging for ever.
38pub struct Signal<T> {
39    state: Arc<Mutex<SignalState<T>>>,
40}
41
42impl<T> Clone for Signal<T> {
43    fn clone(&self) -> Self {
44        Self {
45            state: Arc::clone(&self.state),
46        }
47    }
48}
49
50impl<T> Default for Signal<T> {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl<T> Signal<T> {
57    pub fn new() -> Self {
58        Self {
59            state: Arc::new(Mutex::new(SignalState {
60                value: None,
61                waker: None,
62                closed: false,
63            })),
64        }
65    }
66
67    /// Delivers the value and wakes the waiter. A second call is ignored: one
68    /// signal carries one value.
69    pub fn set(&self, value: T) {
70        let waker = {
71            let mut state = lock(&self.state);
72            if state.closed {
73                return;
74            }
75            state.value = Some(value);
76            state.closed = true;
77            state.waker.take()
78        };
79        if let Some(waker) = waker {
80            waker.wake();
81        }
82    }
83
84    /// Ends the signal with no value, so a waiter stops waiting.
85    pub fn close(&self) {
86        let waker = {
87            let mut state = lock(&self.state);
88            if state.closed {
89                return;
90            }
91            state.closed = true;
92            state.waker.take()
93        };
94        if let Some(waker) = waker {
95            waker.wake();
96        }
97    }
98
99    /// Resolves with the value, or `None` when the signal was closed empty.
100    pub fn wait(&self) -> SignalWait<T> {
101        SignalWait {
102            state: Arc::clone(&self.state),
103        }
104    }
105}
106
107/// The future [`Signal::wait`] returns.
108pub struct SignalWait<T> {
109    state: Arc<Mutex<SignalState<T>>>,
110}
111
112impl<T> Future for SignalWait<T> {
113    type Output = Option<T>;
114
115    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
116        let mut state = lock(&self.state);
117        if let Some(value) = state.value.take() {
118            return Poll::Ready(Some(value));
119        }
120        if state.closed {
121            return Poll::Ready(None);
122        }
123        state.waker = Some(context.waker().clone());
124        Poll::Pending
125    }
126}
127
128struct ChunkState<E> {
129    ready: VecDeque<Vec<u8>>,
130    error: Option<E>,
131    finished: bool,
132    /// The consumer has gone, or asked to stop. The producer notices at its
133    /// next push and stops reading rather than filling a queue nobody drains.
134    abandoned: bool,
135    waker: Option<Waker>,
136}
137
138struct ChunkShared<E> {
139    state: Mutex<ChunkState<E>>,
140    /// The producer parks here while the consumer is behind.
141    room: Condvar,
142}
143
144/// The producing half of a chunked byte stream.
145///
146/// Held by whatever is doing the blocking read. Dropping it without calling
147/// [`ChunkChannel::finish`] or [`ChunkChannel::fail`] ends the stream, so a
148/// worker that panics does not leave a reader waiting for ever.
149pub struct ChunkChannel<E> {
150    shared: Arc<ChunkShared<E>>,
151}
152
153impl<E> ChunkChannel<E> {
154    /// Creates a channel and its reading half.
155    pub fn new() -> (Self, ChunkStream<E>) {
156        let shared = Arc::new(ChunkShared {
157            state: Mutex::new(ChunkState {
158                ready: VecDeque::new(),
159                error: None,
160                finished: false,
161                abandoned: false,
162                waker: None,
163            }),
164            room: Condvar::new(),
165        });
166        (
167            Self {
168                shared: Arc::clone(&shared),
169            },
170            ChunkStream { shared },
171        )
172    }
173
174    /// Publishes one chunk, waiting while the consumer is more than
175    /// [`MAX_PENDING_CHUNKS`] behind.
176    ///
177    /// Returns `false` once the consumer has gone, which is the producer's
178    /// signal to stop reading.
179    pub fn push(&self, chunk: Vec<u8>) -> bool {
180        let waker = {
181            let mut state = lock(&self.shared.state);
182            // The browser has one thread: parking it would stop the very task
183            // that drains the queue, so there the bound is advisory.
184            #[cfg(not(target_arch = "wasm32"))]
185            while state.ready.len() >= MAX_PENDING_CHUNKS && !state.abandoned {
186                state = self
187                    .shared
188                    .room
189                    .wait(state)
190                    .unwrap_or_else(|error| error.into_inner());
191            }
192            if state.abandoned || state.finished {
193                return false;
194            }
195            state.ready.push_back(chunk);
196            state.waker.take()
197        };
198        if let Some(waker) = waker {
199            waker.wake();
200        }
201        true
202    }
203
204    /// Ends the stream with an error.
205    pub fn fail(&self, error: E) {
206        let waker = {
207            let mut state = lock(&self.shared.state);
208            if state.finished {
209                return;
210            }
211            state.error = Some(error);
212            state.finished = true;
213            state.waker.take()
214        };
215        if let Some(waker) = waker {
216            waker.wake();
217        }
218    }
219
220    /// Ends the stream normally.
221    pub fn finish(&self) {
222        let waker = {
223            let mut state = lock(&self.shared.state);
224            if state.finished {
225                return;
226            }
227            state.finished = true;
228            state.waker.take()
229        };
230        if let Some(waker) = waker {
231            waker.wake();
232        }
233    }
234
235    /// Whether the consumer has gone.
236    pub fn is_abandoned(&self) -> bool {
237        lock(&self.shared.state).abandoned
238    }
239}
240
241impl<E> Drop for ChunkChannel<E> {
242    fn drop(&mut self) {
243        self.finish();
244    }
245}
246
247/// The consuming half of a chunked byte stream.
248pub struct ChunkStream<E> {
249    shared: Arc<ChunkShared<E>>,
250}
251
252impl<E> ChunkStream<E> {
253    /// Resolves with the next chunk, `Ok(None)` at the end of the stream, or
254    /// the error the producer ended with.
255    pub fn next(&self) -> ChunkNext<'_, E> {
256        ChunkNext { stream: self }
257    }
258}
259
260impl<E> Drop for ChunkStream<E> {
261    fn drop(&mut self) {
262        let mut state = lock(&self.shared.state);
263        state.abandoned = true;
264        drop(state);
265        // A producer parked on the bound has to learn nobody is reading.
266        self.shared.room.notify_all();
267    }
268}
269
270/// The future [`ChunkStream::next`] returns.
271pub struct ChunkNext<'a, E> {
272    stream: &'a ChunkStream<E>,
273}
274
275impl<E> Future for ChunkNext<'_, E> {
276    type Output = Result<Option<Vec<u8>>, E>;
277
278    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
279        let shared = Arc::clone(&self.stream.shared);
280        let mut state = lock(&shared.state);
281        if let Some(chunk) = state.ready.pop_front() {
282            drop(state);
283            shared.room.notify_one();
284            return Poll::Ready(Ok(Some(chunk)));
285        }
286        if let Some(error) = state.error.take() {
287            return Poll::Ready(Err(error));
288        }
289        if state.finished {
290            return Poll::Ready(Ok(None));
291        }
292        state.waker = Some(context.waker().clone());
293        Poll::Pending
294    }
295}
296
297fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
298    mutex.lock().unwrap_or_else(|error| error.into_inner())
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[derive(Debug, PartialEq, Eq)]
306    struct Failed(&'static str);
307
308    #[test]
309    fn a_signal_carries_one_value_to_whoever_waits() {
310        let signal = Signal::new();
311        signal.set(7u32);
312        assert_eq!(pollster::block_on(signal.wait()), Some(7));
313    }
314
315    /// A worker that dies without answering must not leave a reader hanging.
316    #[test]
317    fn a_closed_signal_resolves_to_nothing_rather_than_waiting_for_ever() {
318        let signal = Signal::<u32>::new();
319        signal.close();
320        assert_eq!(pollster::block_on(signal.wait()), None);
321    }
322
323    #[test]
324    fn a_signal_set_from_another_thread_wakes_the_waiter() {
325        let signal = Signal::new();
326        let worker = signal.clone();
327        let handle = std::thread::spawn(move || {
328            std::thread::sleep(std::time::Duration::from_millis(20));
329            worker.set(11u32);
330        });
331        assert_eq!(pollster::block_on(signal.wait()), Some(11));
332        handle.join().expect("the worker finishes");
333    }
334
335    #[test]
336    fn chunks_arrive_in_the_order_they_were_produced() {
337        let (channel, stream) = ChunkChannel::<Failed>::new();
338        assert!(channel.push(b"one".to_vec()));
339        assert!(channel.push(b"two".to_vec()));
340        channel.finish();
341
342        assert_eq!(pollster::block_on(stream.next()), Ok(Some(b"one".to_vec())));
343        assert_eq!(pollster::block_on(stream.next()), Ok(Some(b"two".to_vec())));
344        assert_eq!(pollster::block_on(stream.next()), Ok(None));
345    }
346
347    #[test]
348    fn a_failed_stream_reports_the_error_after_what_it_already_produced() {
349        let (channel, stream) = ChunkChannel::new();
350        assert!(channel.push(b"partial".to_vec()));
351        channel.fail(Failed("the connection dropped"));
352
353        assert_eq!(
354            pollster::block_on(stream.next()),
355            Ok(Some(b"partial".to_vec()))
356        );
357        assert_eq!(
358            pollster::block_on(stream.next()),
359            Err(Failed("the connection dropped"))
360        );
361    }
362
363    /// A worker that panics must end the stream rather than leaving a reader
364    /// waiting on a chunk nobody will produce.
365    #[test]
366    fn dropping_the_producer_ends_the_stream() {
367        let (channel, stream) = ChunkChannel::<Failed>::new();
368        drop(channel);
369        assert_eq!(pollster::block_on(stream.next()), Ok(None));
370    }
371
372    /// Without a bound a fast server and a slow reader put the whole download in
373    /// memory, which is the thing streaming exists to avoid.
374    #[test]
375    fn the_producer_waits_while_the_consumer_is_behind() {
376        let (channel, stream) = ChunkChannel::<Failed>::new();
377        let pushed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
378        let counter = Arc::clone(&pushed);
379        let worker = std::thread::spawn(move || {
380            for index in 0..MAX_PENDING_CHUNKS + 4 {
381                if !channel.push(vec![index as u8]) {
382                    break;
383                }
384                counter.fetch_add(1, std::sync::atomic::Ordering::Release);
385            }
386            channel.finish();
387        });
388
389        // Give the producer a chance to fill the queue and park on the bound.
390        std::thread::sleep(std::time::Duration::from_millis(50));
391        assert!(
392            pushed.load(std::sync::atomic::Ordering::Acquire) <= MAX_PENDING_CHUNKS,
393            "the producer must stop at the bound rather than reading ahead without limit"
394        );
395
396        let mut received = 0usize;
397        while let Ok(Some(_)) = pollster::block_on(stream.next()) {
398            received += 1;
399        }
400        assert_eq!(received, MAX_PENDING_CHUNKS + 4);
401        worker.join().expect("the worker finishes");
402    }
403
404    /// A reader that stops early — a cancelled download, a dropped screen —
405    /// must stop the producer rather than leaving it reading into a queue
406    /// nobody drains.
407    #[test]
408    fn abandoning_the_stream_stops_the_producer() {
409        let (channel, stream) = ChunkChannel::<Failed>::new();
410        assert!(channel.push(b"first".to_vec()));
411        drop(stream);
412        assert!(channel.is_abandoned());
413        assert!(
414            !channel.push(b"second".to_vec()),
415            "a push after the consumer has gone must report that nobody is reading"
416        );
417    }
418}