Skip to main content

oxigdal_streaming/io/
chunked.rs

1//! Chunked I/O implementation for efficient data access.
2
3use super::buffer::{ChunkDescriptor, ChunkedBuffer};
4use crate::error::{Result, StreamingError};
5use async_trait::async_trait;
6use bytes::Bytes;
7use std::path::{Path, PathBuf};
8use tokio::fs::File;
9use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom};
10use tracing::{debug, info};
11
12/// Strategy for chunking data.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ChunkStrategy {
15    /// Fixed-size chunks
16    FixedSize(usize),
17
18    /// Adaptive chunk size based on available memory
19    Adaptive {
20        /// Minimum chunk size in bytes
21        min_size: usize,
22        /// Maximum chunk size in bytes
23        max_size: usize,
24        /// Target memory ceiling for the chunk size calculation
25        target_memory: usize,
26    },
27
28    /// Line-based chunking (for text data)
29    LineBased {
30        /// Maximum number of lines per chunk
31        max_lines: usize,
32        /// Maximum number of bytes per chunk
33        max_bytes: usize,
34    },
35}
36
37impl ChunkStrategy {
38    /// Get the chunk size for a given chunk index and available memory.
39    pub fn chunk_size_for_index(&self, _index: usize, available_memory: usize) -> usize {
40        match self {
41            ChunkStrategy::FixedSize(size) => *size,
42            ChunkStrategy::Adaptive {
43                min_size,
44                max_size,
45                target_memory,
46            } => {
47                // Calculate adaptive chunk size based on available memory
48                let target_size = available_memory.min(*target_memory);
49                target_size.max(*min_size).min(*max_size)
50            }
51            ChunkStrategy::LineBased { max_bytes, .. } => *max_bytes,
52        }
53    }
54}
55
56impl Default for ChunkStrategy {
57    fn default() -> Self {
58        ChunkStrategy::FixedSize(1024 * 1024) // 1MB default
59    }
60}
61
62/// Trait for chunked I/O operations.
63#[async_trait]
64pub trait ChunkedIO: Send + Sync {
65    /// Read a chunk at the specified offset.
66    async fn read_chunk(&mut self, descriptor: &ChunkDescriptor) -> Result<Bytes>;
67
68    /// Write a chunk at the specified offset.
69    async fn write_chunk(&mut self, descriptor: &ChunkDescriptor, data: Bytes) -> Result<()>;
70
71    /// Get the total size of the data.
72    async fn total_size(&self) -> Result<u64>;
73
74    /// Flush any pending writes.
75    async fn flush(&mut self) -> Result<()>;
76}
77
78/// File-based chunked I/O implementation.
79pub struct FileChunkedIO {
80    /// Path to the file
81    path: PathBuf,
82
83    /// File handle for reading
84    read_file: Option<File>,
85
86    /// File handle for writing
87    write_file: Option<File>,
88
89    /// Chunk strategy (retained for future adaptive sizing support)
90    #[allow(dead_code)]
91    strategy: ChunkStrategy,
92
93    /// Whether to use direct I/O (if supported)
94    direct_io: bool,
95}
96
97impl FileChunkedIO {
98    /// Create a new file-based chunked I/O.
99    pub async fn new<P: AsRef<Path>>(path: P, strategy: ChunkStrategy) -> Result<Self> {
100        let path = path.as_ref().to_path_buf();
101
102        Ok(Self {
103            path,
104            read_file: None,
105            write_file: None,
106            strategy,
107            direct_io: false,
108        })
109    }
110
111    /// Open the file for reading.
112    pub async fn open_read(&mut self) -> Result<()> {
113        if self.read_file.is_some() {
114            return Ok(());
115        }
116
117        let file = File::open(&self.path).await.map_err(StreamingError::Io)?;
118
119        info!("Opened file for reading: {:?}", self.path);
120        self.read_file = Some(file);
121        Ok(())
122    }
123
124    /// Open the file for writing.
125    pub async fn open_write(&mut self) -> Result<()> {
126        if self.write_file.is_some() {
127            return Ok(());
128        }
129
130        let file = File::create(&self.path).await.map_err(StreamingError::Io)?;
131
132        info!("Opened file for writing: {:?}", self.path);
133        self.write_file = Some(file);
134        Ok(())
135    }
136
137    /// Enable direct I/O (platform-dependent).
138    pub fn with_direct_io(mut self, enable: bool) -> Self {
139        self.direct_io = enable;
140        self
141    }
142}
143
144#[async_trait]
145impl ChunkedIO for FileChunkedIO {
146    async fn read_chunk(&mut self, descriptor: &ChunkDescriptor) -> Result<Bytes> {
147        if self.read_file.is_none() {
148            self.open_read().await?;
149        }
150
151        let file = self
152            .read_file
153            .as_mut()
154            .ok_or_else(|| StreamingError::InvalidState("File not open".to_string()))?;
155
156        // Seek to the chunk offset
157        file.seek(SeekFrom::Start(descriptor.offset))
158            .await
159            .map_err(StreamingError::Io)?;
160
161        // Read the chunk data
162        let mut buffer = vec![0u8; descriptor.length];
163        let bytes_read = file
164            .read_exact(&mut buffer)
165            .await
166            .map_err(StreamingError::Io)?;
167
168        debug!(
169            "Read chunk {} at offset {} ({} bytes)",
170            descriptor.index, descriptor.offset, bytes_read
171        );
172
173        Ok(Bytes::from(buffer))
174    }
175
176    async fn write_chunk(&mut self, descriptor: &ChunkDescriptor, data: Bytes) -> Result<()> {
177        if self.write_file.is_none() {
178            self.open_write().await?;
179        }
180
181        let file = self
182            .write_file
183            .as_mut()
184            .ok_or_else(|| StreamingError::InvalidState("File not open".to_string()))?;
185
186        // Seek to the chunk offset
187        file.seek(SeekFrom::Start(descriptor.offset))
188            .await
189            .map_err(StreamingError::Io)?;
190
191        // Write the chunk data
192        file.write_all(&data).await.map_err(StreamingError::Io)?;
193
194        debug!(
195            "Wrote chunk {} at offset {} ({} bytes)",
196            descriptor.index,
197            descriptor.offset,
198            data.len()
199        );
200
201        Ok(())
202    }
203
204    async fn total_size(&self) -> Result<u64> {
205        let metadata = tokio::fs::metadata(&self.path)
206            .await
207            .map_err(StreamingError::Io)?;
208
209        Ok(metadata.len())
210    }
211
212    async fn flush(&mut self) -> Result<()> {
213        if let Some(file) = &mut self.write_file {
214            file.flush().await.map_err(StreamingError::Io)?;
215            file.sync_all().await.map_err(StreamingError::Io)?;
216        }
217        Ok(())
218    }
219}
220
221/// Memory-based chunked I/O implementation for testing.
222pub struct MemoryChunkedIO {
223    /// The in-memory buffer
224    buffer: Vec<u8>,
225
226    /// Chunk strategy (retained for future adaptive sizing support)
227    #[allow(dead_code)]
228    strategy: ChunkStrategy,
229}
230
231impl MemoryChunkedIO {
232    /// Create a new memory-based chunked I/O.
233    pub fn new(size: usize, strategy: ChunkStrategy) -> Self {
234        Self {
235            buffer: vec![0u8; size],
236            strategy,
237        }
238    }
239
240    /// Get a reference to the buffer.
241    pub fn buffer(&self) -> &[u8] {
242        &self.buffer
243    }
244}
245
246#[async_trait]
247impl ChunkedIO for MemoryChunkedIO {
248    async fn read_chunk(&mut self, descriptor: &ChunkDescriptor) -> Result<Bytes> {
249        let start = descriptor.offset as usize;
250        let end = start + descriptor.length;
251
252        if end > self.buffer.len() {
253            return Err(StreamingError::InvalidOperation(
254                "Chunk exceeds buffer size".to_string(),
255            ));
256        }
257
258        let data = self.buffer[start..end].to_vec();
259        Ok(Bytes::from(data))
260    }
261
262    async fn write_chunk(&mut self, descriptor: &ChunkDescriptor, data: Bytes) -> Result<()> {
263        let start = descriptor.offset as usize;
264        let end = start + descriptor.length;
265
266        if end > self.buffer.len() {
267            return Err(StreamingError::InvalidOperation(
268                "Chunk exceeds buffer size".to_string(),
269            ));
270        }
271
272        self.buffer[start..end].copy_from_slice(&data);
273        Ok(())
274    }
275
276    async fn total_size(&self) -> Result<u64> {
277        Ok(self.buffer.len() as u64)
278    }
279
280    async fn flush(&mut self) -> Result<()> {
281        // No-op for memory-based I/O
282        Ok(())
283    }
284}
285
286/// Chunked I/O with prefetching and caching.
287pub struct CachedChunkedIO<T: ChunkedIO> {
288    /// The underlying chunked I/O
289    inner: T,
290
291    /// Chunk buffer for caching
292    cache: ChunkedBuffer,
293
294    /// Number of chunks to prefetch
295    prefetch_count: usize,
296}
297
298impl<T: ChunkedIO> CachedChunkedIO<T> {
299    /// Create a new cached chunked I/O.
300    pub fn new(inner: T, cache_size: usize, prefetch_count: usize) -> Self {
301        Self {
302            inner,
303            cache: ChunkedBuffer::new(1024 * 1024, cache_size),
304            prefetch_count,
305        }
306    }
307
308    /// Prefetch chunks starting from the given index.
309    pub async fn prefetch(&mut self, start_index: usize, total_size: u64) -> Result<()> {
310        let total_chunks = self.cache.calculate_chunks(total_size);
311
312        for i in 0..self.prefetch_count {
313            let index = start_index + i;
314            if index >= total_chunks {
315                break;
316            }
317
318            let descriptor = self.cache.descriptor_for_index(index, total_size);
319            let data = self.inner.read_chunk(&descriptor).await?;
320            self.cache.push(descriptor, data).await?;
321        }
322
323        Ok(())
324    }
325}
326
327#[async_trait]
328impl<T: ChunkedIO> ChunkedIO for CachedChunkedIO<T> {
329    async fn read_chunk(&mut self, descriptor: &ChunkDescriptor) -> Result<Bytes> {
330        // Check if chunk is in cache
331        if let Some((_, data)) = self.cache.pop().await? {
332            return Ok(data);
333        }
334
335        // Not in cache, read from underlying I/O
336        self.inner.read_chunk(descriptor).await
337    }
338
339    async fn write_chunk(&mut self, descriptor: &ChunkDescriptor, data: Bytes) -> Result<()> {
340        self.inner.write_chunk(descriptor, data).await
341    }
342
343    async fn total_size(&self) -> Result<u64> {
344        self.inner.total_size().await
345    }
346
347    async fn flush(&mut self) -> Result<()> {
348        self.inner.flush().await
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[tokio::test]
357    async fn test_memory_chunked_io() {
358        let mut io = MemoryChunkedIO::new(10240, ChunkStrategy::FixedSize(1024));
359
360        let descriptor = ChunkDescriptor::new(0, 1024, 0, 10);
361        let data = Bytes::from(vec![42u8; 1024]);
362
363        io.write_chunk(&descriptor, data.clone()).await.ok();
364
365        let read_data = io.read_chunk(&descriptor).await.ok();
366        assert!(read_data.is_some());
367        assert_eq!(read_data.expect("chunk read should succeed").len(), 1024);
368    }
369
370    #[test]
371    fn test_chunk_strategy() {
372        let strategy = ChunkStrategy::FixedSize(1024);
373        assert_eq!(strategy.chunk_size_for_index(0, 2048), 1024);
374
375        let adaptive = ChunkStrategy::Adaptive {
376            min_size: 512,
377            max_size: 2048,
378            target_memory: 1024,
379        };
380        // available_memory=1500, target_memory=1024 → target_size=min(1500,1024)=1024
381        assert_eq!(adaptive.chunk_size_for_index(0, 1500), 1024);
382        // available_memory=500, target_memory=1024 → target_size=min(500,1024)=500 → clamped by min→512
383        assert_eq!(adaptive.chunk_size_for_index(0, 500), 512);
384        // available_memory=3000, target_memory=1024 → target_size=min(3000,1024)=1024
385        // (target_memory is the upper cap, so we get 1024 not 2048)
386        assert_eq!(adaptive.chunk_size_for_index(0, 3000), 1024);
387    }
388}