cranpose_services/content/
threaded.rs1use 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
22pub 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
57pub 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
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;