oxigeo_streaming/io/
reader.rs1use super::buffer::ChunkedBuffer;
4use super::chunked::{ChunkStrategy, ChunkedIO, FileChunkedIO};
5use crate::error::{Result, StreamingError};
6use bytes::Bytes;
7use std::path::Path;
8use std::sync::Arc;
9use tokio::sync::Semaphore;
10use tracing::info;
11
12pub struct ChunkedReader {
14 io: Box<dyn ChunkedIO>,
16
17 buffer: ChunkedBuffer,
19
20 current_index: usize,
22
23 total_chunks: usize,
25
26 total_size: u64,
28
29 prefetch_semaphore: Arc<Semaphore>,
31
32 prefetch_count: usize,
34}
35
36impl ChunkedReader {
37 pub async fn from_file<P: AsRef<Path>>(
39 path: P,
40 strategy: ChunkStrategy,
41 buffer_size: usize,
42 prefetch_count: usize,
43 ) -> Result<Self> {
44 let mut io = FileChunkedIO::new(path, strategy).await?;
45 io.open_read().await?;
46
47 let total_size = io.total_size().await?;
48 let chunk_size = strategy.chunk_size_for_index(0, 0);
49 let buffer = ChunkedBuffer::new(chunk_size, buffer_size);
50 let total_chunks = buffer.calculate_chunks(total_size);
51
52 info!(
53 "Created chunked reader: {} chunks, {} bytes total",
54 total_chunks, total_size
55 );
56
57 Ok(Self {
58 io: Box::new(io),
59 buffer,
60 current_index: 0,
61 total_chunks,
62 total_size,
63 prefetch_semaphore: Arc::new(Semaphore::new(prefetch_count)),
64 prefetch_count,
65 })
66 }
67
68 pub async fn read_chunk(&mut self) -> Result<Option<Bytes>> {
70 if self.current_index >= self.total_chunks {
71 return Ok(None);
72 }
73
74 if self
84 .buffer
85 .peek()
86 .await?
87 .is_some_and(|descriptor| descriptor.index == self.current_index)
88 && let Some((_, data)) = self.buffer.pop().await?
89 {
90 self.current_index += 1;
91 self.start_prefetch().await?;
92 return Ok(Some(data));
93 }
94
95 let descriptor = self
97 .buffer
98 .descriptor_for_index(self.current_index, self.total_size);
99 let data = self.io.read_chunk(&descriptor).await?;
100
101 self.current_index += 1;
102 self.start_prefetch().await?;
103
104 Ok(Some(data))
105 }
106
107 async fn start_prefetch(&mut self) -> Result<()> {
118 if !self.buffer.is_empty().await {
121 return Ok(());
122 }
123
124 let start_index = self.current_index;
125 let end_index = (start_index + self.prefetch_count).min(self.total_chunks);
126 if start_index >= end_index {
127 return Ok(());
128 }
129
130 if !self.buffer.rebase_if_empty(start_index).await {
134 return Ok(());
135 }
136
137 for index in start_index..end_index {
138 if self.prefetch_semaphore.available_permits() == 0 {
139 break;
140 }
141
142 let descriptor = self.buffer.descriptor_for_index(index, self.total_size);
143
144 let _permit = self
146 .prefetch_semaphore
147 .try_acquire()
148 .map_err(|_| StreamingError::Other("Failed to acquire permit".to_string()))?;
149
150 let data = self.io.read_chunk(&descriptor).await?;
151 self.buffer.push(descriptor, data).await?;
152 }
153
154 Ok(())
155 }
156
157 pub fn total_chunks(&self) -> usize {
159 self.total_chunks
160 }
161
162 pub fn current_index(&self) -> usize {
164 self.current_index
165 }
166
167 pub fn has_more(&self) -> bool {
169 self.current_index < self.total_chunks
170 }
171
172 pub fn progress(&self) -> f64 {
174 if self.total_chunks == 0 {
175 100.0
176 } else {
177 (self.current_index as f64 / self.total_chunks as f64) * 100.0
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::env;
186 use std::sync::atomic::{AtomicU64, Ordering};
187 use tokio::fs::File;
188 use tokio::io::AsyncWriteExt;
189
190 struct TempPath(std::path::PathBuf);
198
199 impl TempPath {
200 fn new(name: &str) -> Self {
201 static COUNTER: AtomicU64 = AtomicU64::new(0);
202 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
203 Self(env::temp_dir().join(format!(
204 "oxigeo_streaming_io_reader_{}_{seq}_{name}",
205 std::process::id()
206 )))
207 }
208 }
209
210 impl std::ops::Deref for TempPath {
211 type Target = std::path::Path;
212
213 fn deref(&self) -> &std::path::Path {
214 &self.0
215 }
216 }
217
218 impl AsRef<std::path::Path> for TempPath {
219 fn as_ref(&self) -> &std::path::Path {
220 &self.0
221 }
222 }
223
224 impl Drop for TempPath {
225 fn drop(&mut self) {
226 let _ = std::fs::remove_file(&self.0);
227 }
228 }
229
230 #[tokio::test]
231 async fn test_chunked_reader() {
232 let test_path = TempPath::new("chunked_read.dat");
233
234 let mut f = File::create(&test_path)
236 .await
237 .expect("fixture file should be creatable");
238 let data = vec![42u8; 10240];
239 f.write_all(&data)
240 .await
241 .expect("fixture should be writable");
242 f.flush().await.expect("fixture should flush");
243 drop(f);
244
245 let reader = ChunkedReader::from_file(&test_path, ChunkStrategy::FixedSize(1024), 10240, 2)
247 .await
248 .expect("a 10 KiB readable file must open as a ChunkedReader");
249
250 let mut reader = reader;
251 assert_eq!(
252 reader.total_chunks(),
253 10,
254 "10240 bytes at 1024 bytes/chunk is 10 chunks"
255 );
256 assert_eq!(reader.current_index(), 0, "nothing read yet");
257 assert!(reader.has_more(), "a fresh reader has chunks pending");
258
259 let mut chunks = 0usize;
261 let mut total = 0usize;
262 while let Some(chunk) = reader
263 .read_chunk()
264 .await
265 .expect("reading a well-formed chunk must succeed")
266 {
267 assert_eq!(
268 chunk.len(),
269 1024,
270 "chunk {chunks} must be a full 1024 bytes"
271 );
272 assert!(
273 chunk.iter().all(|&b| b == 42),
274 "chunk {chunks} must return the bytes that were written"
275 );
276 chunks += 1;
277 total += chunk.len();
278 }
279
280 assert_eq!(chunks, 10, "the reader must yield every chunk exactly once");
281 assert_eq!(total, 10240, "the reader must yield every byte of the file");
282 assert!(!reader.has_more(), "a drained reader has nothing pending");
283 assert!(
284 (reader.progress() - 100.0).abs() < 1e-9,
285 "a drained reader must report 100% progress, got {}",
286 reader.progress()
287 );
288 }
289}