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