Skip to main content

cranpose_services/content/
threaded.rs

1//! Blocking reads and writes off the thread that awaits them.
2//!
3//! A file, a Storage Access Framework document or a security-scoped file is
4//! read and written with blocking calls. Behind the async [`ContentReader`]
5//! and [`ContentSink`] those calls run here, on a thread per reader or sink,
6//! and the thread that awaits them, usually the UI thread, only hands chunks
7//! over: copying a gigabyte leaves the screen drawing. One-shot work runs on
8//! the shared bounded blocking executor instead.
9
10use std::{
11    io::{self, ErrorKind, Read},
12    rc::Rc,
13    sync::mpsc::{Sender, channel},
14};
15
16use super::{
17    Content, ContentError, ContentFuture, ContentReader, ContentReaderRef, ContentSink,
18    ContentSinkRef, DEFAULT_CHUNK_LEN,
19};
20use crate::async_io::{ChunkChannel, ChunkStream, Signal};
21
22/// Opens a source with `open` on a thread of its own and reads it there,
23/// [`DEFAULT_CHUNK_LEN`] bytes at a time and at most
24/// [`MAX_PENDING_CHUNKS`](crate::async_io::MAX_PENDING_CHUNKS) chunks ahead of
25/// the reader. The future resolves once the source is open; `map_error`
26/// describes a read that fails later.
27pub fn open_threaded<R, O, M>(
28    open: O,
29    map_error: M,
30) -> ContentFuture<'static, Result<ContentReaderRef, ContentError>>
31where
32    R: Read,
33    O: FnOnce() -> Result<R, ContentError> + Send + 'static,
34    M: Fn(io::Error) -> ContentError + Send + 'static,
35{
36    let (chunks, stream) = ChunkChannel::new();
37    let opened = Signal::new();
38    let reported = opened.clone();
39    let spawned = std::thread::Builder::new()
40        .name("cranpose-content-read".into())
41        .spawn(move || match open() {
42            Ok(source) => {
43                reported.set(Ok(()));
44                read_into(source, &chunks, &map_error);
45            }
46            Err(error) => reported.set(Err(error)),
47        });
48    if let Err(error) = spawned {
49        opened.set(Err(not_started(&error)));
50    }
51    Box::pin(async move {
52        opened.wait().await.unwrap_or_else(|| Err(gone()))?;
53        Ok(Rc::new(ThreadedReader { stream }) as ContentReaderRef)
54    })
55}
56
57/// Runs `work` on the shared bounded blocking executor
58/// ([`cranpose_core::withBlocking`]) and resolves with what it returns. A
59/// burst of work waits for the executor's threads without blocking the
60/// caller.
61pub fn run_threaded<T: Send + 'static>(
62    work: impl FnOnce() -> Result<T, ContentError> + Send + 'static,
63) -> ContentFuture<'static, Result<T, ContentError>> {
64    Box::pin(async move {
65        cranpose_core::withBlocking(work)
66            .await
67            .unwrap_or_else(|error| Err(ContentError::Io(format!("content work: {error}"))))
68    })
69}
70
71/// What a [`threaded_sink`] writes into, on the sink's thread.
72pub trait BlockingSink: Send + 'static {
73    /// Appends `bytes`.
74    fn write(&mut self, bytes: &[u8]) -> Result<(), ContentError>;
75
76    /// Commits what was written. Dropping the sink without it discards what
77    /// the destination allows to discard.
78    fn finish(&mut self) -> Result<(), ContentError>;
79}
80
81/// A [`ContentSink`] that writes into `sink` on a thread of its own; each
82/// write resolves once `sink` took the bytes.
83pub fn threaded_sink(sink: impl BlockingSink) -> ContentSinkRef {
84    let (jobs, taken) = channel::<Job>();
85    let spawned = std::thread::Builder::new()
86        .name("cranpose-content-write".into())
87        .spawn(move || {
88            let mut sink = sink;
89            for job in taken {
90                match job {
91                    Job::Write(bytes, done) => done.set(sink.write(&bytes)),
92                    Job::Finish(done) => {
93                        done.set(sink.finish());
94                        return;
95                    }
96                }
97            }
98        });
99    if let Err(error) = spawned {
100        log::warn!("cranpose: {}", not_started(&error));
101    }
102    Rc::new(ThreadedSink { jobs })
103}
104
105/// Reads from `source` until `buffer` is full or the source ends, and returns
106/// how many bytes came: fewer than the buffer holds only at the end.
107pub fn read_full(source: &mut impl Read, buffer: &mut [u8]) -> io::Result<usize> {
108    let mut filled = 0;
109    while filled < buffer.len() {
110        match source.read(&mut buffer[filled..]) {
111            Ok(0) => break,
112            Ok(read) => filled += read,
113            Err(error) if error.kind() == ErrorKind::Interrupted => {}
114            Err(error) => return Err(error),
115        }
116    }
117    Ok(filled)
118}
119
120/// Copies `from` into `to` and commits it; `progress` hears the bytes copied
121/// so far after each chunk. Resolves with the bytes copied.
122pub async fn copy_content(
123    from: &dyn Content,
124    to: &ContentSinkRef,
125    mut progress: impl FnMut(u64),
126) -> Result<u64, ContentError> {
127    let reader = from.open().await?;
128    let mut copied = 0u64;
129    while let Some(chunk) = reader.read_chunk().await? {
130        copied += chunk.len() as u64;
131        to.write_chunk(chunk).await?;
132        progress(copied);
133    }
134    to.finish().await?;
135    Ok(copied)
136}
137
138fn read_into(
139    mut source: impl Read,
140    chunks: &ChunkChannel<ContentError>,
141    map_error: &impl Fn(io::Error) -> ContentError,
142) {
143    loop {
144        let mut buffer = vec![0u8; DEFAULT_CHUNK_LEN];
145        let filled = match read_full(&mut source, &mut buffer) {
146            Ok(filled) => filled,
147            Err(error) => {
148                chunks.fail(map_error(error));
149                return;
150            }
151        };
152        // A chunk that is not full ended at the end of the source.
153        let ended = filled < DEFAULT_CHUNK_LEN;
154        if filled > 0 {
155            buffer.truncate(filled);
156            if !chunks.push(buffer) {
157                return;
158            }
159        }
160        if ended {
161            chunks.finish();
162            return;
163        }
164    }
165}
166
167struct ThreadedReader {
168    stream: ChunkStream<ContentError>,
169}
170
171impl ContentReader for ThreadedReader {
172    fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>> {
173        Box::pin(self.stream.next())
174    }
175}
176
177enum Job {
178    Write(Vec<u8>, Signal<Result<(), ContentError>>),
179    Finish(Signal<Result<(), ContentError>>),
180}
181
182struct ThreadedSink {
183    jobs: Sender<Job>,
184}
185
186impl ThreadedSink {
187    fn submit(
188        &self,
189        job: impl FnOnce(Signal<Result<(), ContentError>>) -> Job,
190    ) -> ContentFuture<'_, Result<(), ContentError>> {
191        let done = Signal::new();
192        let sent = self.jobs.send(job(done.clone()));
193        Box::pin(async move {
194            sent.map_err(|_| gone())?;
195            done.wait().await.unwrap_or_else(|| Err(gone()))
196        })
197    }
198}
199
200impl ContentSink for ThreadedSink {
201    fn write_chunk(&self, bytes: Vec<u8>) -> ContentFuture<'_, Result<(), ContentError>> {
202        self.submit(|done| Job::Write(bytes, done))
203    }
204
205    fn finish(&self) -> ContentFuture<'_, Result<(), ContentError>> {
206        self.submit(Job::Finish)
207    }
208}
209
210fn not_started(error: &io::Error) -> ContentError {
211    ContentError::Io(format!("the content thread did not start: {error}"))
212}
213
214fn gone() -> ContentError {
215    ContentError::Io("the content thread stopped".into())
216}
217
218#[cfg(test)]
219#[path = "tests/threaded_tests.rs"]
220mod tests;