cranpose_services/content/
threaded.rs1use std::{
10 io::{self, ErrorKind, Read},
11 rc::Rc,
12 sync::mpsc::{Sender, channel},
13};
14
15use super::{
16 Content, ContentError, ContentFuture, ContentReader, ContentReaderRef, ContentSink,
17 ContentSinkRef, DEFAULT_CHUNK_LEN,
18};
19use crate::async_io::{ChunkChannel, ChunkStream, Signal};
20
21pub fn open_threaded<R, O, M>(
27 open: O,
28 map_error: M,
29) -> ContentFuture<'static, Result<ContentReaderRef, ContentError>>
30where
31 R: Read,
32 O: FnOnce() -> Result<R, ContentError> + Send + 'static,
33 M: Fn(io::Error) -> ContentError + Send + 'static,
34{
35 let (chunks, stream) = ChunkChannel::new();
36 let opened = Signal::new();
37 let reported = opened.clone();
38 let spawned = std::thread::Builder::new()
39 .name("cranpose-content-read".into())
40 .spawn(move || match open() {
41 Ok(source) => {
42 reported.set(Ok(()));
43 read_into(source, &chunks, &map_error);
44 }
45 Err(error) => reported.set(Err(error)),
46 });
47 if let Err(error) = spawned {
48 opened.set(Err(not_started(&error)));
49 }
50 Box::pin(async move {
51 opened.wait().await.unwrap_or_else(|| Err(gone()))?;
52 Ok(Rc::new(ThreadedReader { stream }) as ContentReaderRef)
53 })
54}
55
56pub fn run_threaded<T: Send + 'static>(
58 work: impl FnOnce() -> Result<T, ContentError> + Send + 'static,
59) -> ContentFuture<'static, Result<T, ContentError>> {
60 let done = Signal::new();
61 let reported = done.clone();
62 let spawned = std::thread::Builder::new()
63 .name("cranpose-content-work".into())
64 .spawn(move || reported.set(work()));
65 if let Err(error) = spawned {
66 done.set(Err(not_started(&error)));
67 }
68 Box::pin(async move { done.wait().await.unwrap_or_else(|| Err(gone())) })
69}
70
71pub trait BlockingSink: Send + 'static {
73 fn write(&mut self, bytes: &[u8]) -> Result<(), ContentError>;
75
76 fn finish(&mut self) -> Result<(), ContentError>;
79}
80
81pub 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
105pub 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
120pub 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 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;