oxigdal_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 let Some((_, data)) = self.buffer.pop().await? {
76 self.current_index += 1;
77 self.start_prefetch().await?;
78 return Ok(Some(data));
79 }
80
81 let descriptor = self
83 .buffer
84 .descriptor_for_index(self.current_index, self.total_size);
85 let data = self.io.read_chunk(&descriptor).await?;
86
87 self.current_index += 1;
88 self.start_prefetch().await?;
89
90 Ok(Some(data))
91 }
92
93 async fn start_prefetch(&mut self) -> Result<()> {
95 let start_index = self.current_index;
96 let end_index = (start_index + self.prefetch_count).min(self.total_chunks);
97
98 for index in start_index..end_index {
99 if self.prefetch_semaphore.available_permits() == 0 {
100 break;
101 }
102
103 let descriptor = self.buffer.descriptor_for_index(index, self.total_size);
104
105 if let Some(peek_desc) = self.buffer.peek().await?
107 && peek_desc.index == descriptor.index
108 {
109 continue;
110 }
111
112 let _permit = self
114 .prefetch_semaphore
115 .try_acquire()
116 .map_err(|_| StreamingError::Other("Failed to acquire permit".to_string()))?;
117
118 let data = self.io.read_chunk(&descriptor).await?;
119 self.buffer.push(descriptor, data).await?;
120 }
121
122 Ok(())
123 }
124
125 pub fn total_chunks(&self) -> usize {
127 self.total_chunks
128 }
129
130 pub fn current_index(&self) -> usize {
132 self.current_index
133 }
134
135 pub fn has_more(&self) -> bool {
137 self.current_index < self.total_chunks
138 }
139
140 pub fn progress(&self) -> f64 {
142 if self.total_chunks == 0 {
143 100.0
144 } else {
145 (self.current_index as f64 / self.total_chunks as f64) * 100.0
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::env;
154 use tokio::fs::File;
155 use tokio::io::AsyncWriteExt;
156
157 #[tokio::test]
158 async fn test_chunked_reader() {
159 let temp_dir = env::temp_dir();
160 let test_path = temp_dir.join("test_chunked_read.dat");
161
162 let file = File::create(&test_path).await.ok();
164 if let Some(mut f) = file {
165 let data = vec![42u8; 10240];
166 f.write_all(&data).await.ok();
167 }
168
169 let result =
171 ChunkedReader::from_file(&test_path, ChunkStrategy::FixedSize(1024), 10240, 2).await;
172
173 tokio::fs::remove_file(&test_path).await.ok();
175
176 assert!(result.is_ok() || result.is_err());
177 }
178}