Skip to main content

scirs2_io/
async_io.rs

1//! Async I/O support for streaming capabilities
2//!
3//! This module provides asynchronous I/O interfaces for non-blocking processing
4//! of large datasets. It builds on the streaming module to provide async versions
5//! of file reading, writing, and processing operations.
6//!
7//! ## Features
8//!
9//! - **Async File Reading**: Non-blocking file reading with tokio
10//! - **Async Streaming**: Asynchronous stream processing with backpressure
11//! - **Concurrent Processing**: Process multiple chunks concurrently
12//! - **Network I/O**: Support for async network operations
13//! - **Cancellation**: Support for operation cancellation
14//! - **Progress Tracking**: Real-time progress monitoring for async operations
15//!
16//! ## Examples
17//!
18//! ```rust,no_run
19//! use scirs2_io::async_io::{AsyncChunkedReader, AsyncStreamingConfig};
20//! use futures::StreamExt;
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//!     let config = AsyncStreamingConfig::new()
25//!         .chunk_size(64 * 1024)
26//!         .concurrency(4);
27//!
28//!     let mut reader = AsyncChunkedReader::new("large_file.dat", config).await?;
29//!     
30//!     while let Some(chunk_result) = reader.next().await {
31//!         let chunk = chunk_result?;
32//!         // Process chunk asynchronously
33//!         tokio::task::yield_now().await; // Yield to allow other tasks
34//!     }
35//!     Ok(())
36//! }
37//! ```
38
39#![allow(dead_code)]
40#![allow(clippy::too_many_arguments)]
41
42use std::future::Future;
43use std::path::Path;
44use std::pin::Pin;
45use std::task::{Context, Poll};
46
47#[cfg(feature = "async")]
48use futures::{Stream, StreamExt};
49#[cfg(feature = "async")]
50use tokio::fs::File;
51#[cfg(feature = "async")]
52use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
53
54use crate::error::{IoError, Result};
55
56/// Configuration for async streaming operations
57#[derive(Debug, Clone)]
58pub struct AsyncStreamingConfig {
59    /// Size of each chunk in bytes (default: 64KB)
60    pub chunk_size: usize,
61    /// Buffer size for I/O operations (default: 8KB)
62    pub buffer_size: usize,
63    /// Maximum concurrent operations (default: 4)
64    pub concurrency: usize,
65    /// Enable automatic backpressure handling
66    pub enable_backpressure: bool,
67    /// Timeout for individual operations in milliseconds
68    pub operation_timeout_ms: Option<u64>,
69    /// Maximum number of chunks to process (None for unlimited)
70    pub max_chunks: Option<usize>,
71    /// Skip the first N chunks
72    pub skip_chunks: usize,
73}
74
75impl Default for AsyncStreamingConfig {
76    fn default() -> Self {
77        Self {
78            chunk_size: 64 * 1024, // 64KB
79            buffer_size: 8 * 1024, // 8KB
80            concurrency: 4,
81            enable_backpressure: true,
82            operation_timeout_ms: None,
83            max_chunks: None,
84            skip_chunks: 0,
85        }
86    }
87}
88
89impl AsyncStreamingConfig {
90    /// Create a new async streaming configuration with default values
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Set the chunk size
96    pub fn chunk_size(mut self, size: usize) -> Self {
97        self.chunk_size = size;
98        self
99    }
100
101    /// Set the buffer size
102    pub fn buffer_size(mut self, size: usize) -> Self {
103        self.buffer_size = size;
104        self
105    }
106
107    /// Set the concurrency level
108    pub fn concurrency(mut self, level: usize) -> Self {
109        self.concurrency = level;
110        self
111    }
112
113    /// Enable or disable backpressure handling
114    pub fn enable_backpressure(mut self, enable: bool) -> Self {
115        self.enable_backpressure = enable;
116        self
117    }
118
119    /// Set operation timeout
120    pub fn timeout(mut self, timeoutms: u64) -> Self {
121        self.operation_timeout_ms = Some(timeoutms);
122        self
123    }
124
125    /// Set maximum number of chunks to process
126    pub fn max_chunks(mut self, max: usize) -> Self {
127        self.max_chunks = Some(max);
128        self
129    }
130
131    /// Set number of chunks to skip
132    pub fn skip_chunks(mut self, skip: usize) -> Self {
133        self.skip_chunks = skip;
134        self
135    }
136}
137
138/// Async iterator for reading files in chunks
139pub struct AsyncChunkedReader {
140    reader: BufReader<File>,
141    config: AsyncStreamingConfig,
142    chunks_read: usize,
143    total_bytes_read: u64,
144    finished: bool,
145}
146
147impl AsyncChunkedReader {
148    /// Create a new async chunked reader for the specified file
149    pub async fn new<P: AsRef<Path>>(path: P, config: AsyncStreamingConfig) -> Result<Self> {
150        let file = File::open(path.as_ref())
151            .await
152            .map_err(|e| IoError::FileError(format!("Failed to open file: {}", e)))?;
153
154        let reader = BufReader::with_capacity(config.buffer_size, file);
155
156        Ok(Self {
157            reader,
158            config,
159            chunks_read: 0,
160            total_bytes_read: 0,
161            finished: false,
162        })
163    }
164
165    /// Get the total number of bytes read so far
166    pub fn bytes_read(&self) -> u64 {
167        self.total_bytes_read
168    }
169
170    /// Get the number of chunks read so far
171    pub fn chunks_read(&self) -> usize {
172        self.chunks_read
173    }
174
175    /// Check if the reader has finished
176    pub fn is_finished(&self) -> bool {
177        self.finished
178    }
179
180    /// Read the next chunk asynchronously
181    pub async fn read_next_chunk(&mut self) -> Result<Option<Vec<u8>>> {
182        if self.finished {
183            return Ok(None);
184        }
185
186        // Check if we should skip chunks
187        if self.chunks_read < self.config.skip_chunks {
188            let mut buffer = vec![0u8; self.config.chunk_size];
189            match self.reader.read(&mut buffer).await {
190                Ok(0) => {
191                    self.finished = true;
192                    return Ok(None);
193                }
194                Ok(bytes_read) => {
195                    self.total_bytes_read += bytes_read as u64;
196                    self.chunks_read += 1;
197                    return Box::pin(self.read_next_chunk()).await; // Recursive call to skip
198                }
199                Err(e) => {
200                    self.finished = true;
201                    return Err(IoError::FileError(format!("Failed to skip chunk: {}", e)));
202                }
203            }
204        }
205
206        // Check max chunks limit
207        if let Some(max) = self.config.max_chunks {
208            if self.chunks_read >= max + self.config.skip_chunks {
209                self.finished = true;
210                return Ok(None);
211            }
212        }
213
214        let mut chunk = vec![0u8; self.config.chunk_size];
215
216        // Apply timeout if configured
217        let read_future = self.reader.read(&mut chunk);
218
219        let bytes_read = if let Some(timeout_ms) = self.config.operation_timeout_ms {
220            match tokio::time::timeout(tokio::time::Duration::from_millis(timeout_ms), read_future)
221                .await
222            {
223                Ok(Ok(bytes)) => bytes,
224                Ok(Err(e)) => {
225                    self.finished = true;
226                    return Err(IoError::FileError(format!("Failed to read chunk: {}", e)));
227                }
228                Err(_) => {
229                    self.finished = true;
230                    return Err(IoError::FileError("Read operation timed out".to_string()));
231                }
232            }
233        } else {
234            match read_future.await {
235                Ok(bytes) => bytes,
236                Err(e) => {
237                    self.finished = true;
238                    return Err(IoError::FileError(format!("Failed to read chunk: {}", e)));
239                }
240            }
241        };
242
243        if bytes_read == 0 {
244            // End of file
245            self.finished = true;
246            Ok(None)
247        } else {
248            chunk.truncate(bytes_read);
249            self.total_bytes_read += bytes_read as u64;
250            self.chunks_read += 1;
251            Ok(Some(chunk))
252        }
253    }
254}
255
256impl Stream for AsyncChunkedReader {
257    type Item = Result<Vec<u8>>;
258
259    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
260        if self.finished {
261            return Poll::Ready(None);
262        }
263
264        // Create a future for the next chunk read
265        let read_future = self.read_next_chunk();
266        tokio::pin!(read_future);
267
268        match read_future.poll(cx) {
269            Poll::Ready(Ok(Some(chunk))) => Poll::Ready(Some(Ok(chunk))),
270            Poll::Ready(Ok(None)) => Poll::Ready(None),
271            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
272            Poll::Pending => Poll::Pending,
273        }
274    }
275}
276
277/// Async line-based reader
278pub struct AsyncLineReader {
279    reader: BufReader<File>,
280    config: AsyncStreamingConfig,
281    lines_read: usize,
282    finished: bool,
283}
284
285impl AsyncLineReader {
286    /// Create a new async line reader
287    pub async fn new<P: AsRef<Path>>(path: P, config: AsyncStreamingConfig) -> Result<Self> {
288        let file = File::open(path.as_ref())
289            .await
290            .map_err(|e| IoError::FileError(format!("Failed to open file: {}", e)))?;
291
292        let reader = BufReader::with_capacity(config.buffer_size, file);
293
294        Ok(Self {
295            reader,
296            config,
297            lines_read: 0,
298            finished: false,
299        })
300    }
301
302    /// Get the number of lines read so far
303    pub fn lines_read(&self) -> usize {
304        self.lines_read
305    }
306
307    /// Check if the reader has finished
308    pub fn is_finished(&self) -> bool {
309        self.finished
310    }
311
312    /// Read the next batch of lines asynchronously
313    pub async fn read_next_lines(&mut self) -> Result<Option<Vec<String>>> {
314        if self.finished {
315            return Ok(None);
316        }
317
318        // Check if we should skip lines
319        if self.lines_read < self.config.skip_chunks {
320            let mut line = String::new();
321            match self.reader.read_line(&mut line).await {
322                Ok(0) => {
323                    self.finished = true;
324                    return Ok(None);
325                }
326                Ok(_) => {
327                    self.lines_read += 1;
328                    return Box::pin(self.read_next_lines()).await; // Recursive call to skip
329                }
330                Err(e) => {
331                    self.finished = true;
332                    return Err(IoError::FileError(format!("Failed to skip line: {}", e)));
333                }
334            }
335        }
336
337        // Check max chunks limit
338        if let Some(max) = self.config.max_chunks {
339            if self.lines_read >= max + self.config.skip_chunks {
340                self.finished = true;
341                return Ok(None);
342            }
343        }
344
345        let mut lines = Vec::new();
346        let target_lines = self.config.chunk_size; // Treat chunk_size as number of lines
347
348        for _ in 0..target_lines {
349            let mut line = String::new();
350
351            let read_result = if let Some(timeout_ms) = self.config.operation_timeout_ms {
352                match tokio::time::timeout(
353                    tokio::time::Duration::from_millis(timeout_ms),
354                    self.reader.read_line(&mut line),
355                )
356                .await
357                {
358                    Ok(result) => result,
359                    Err(_) => {
360                        self.finished = true;
361                        return Err(IoError::FileError(
362                            "Read line operation timed out".to_string(),
363                        ));
364                    }
365                }
366            } else {
367                self.reader.read_line(&mut line).await
368            };
369
370            match read_result {
371                Ok(0) => {
372                    // End of file
373                    self.finished = true;
374                    break;
375                }
376                Ok(_) => {
377                    // Remove trailing newline
378                    if line.ends_with('\n') {
379                        line.pop();
380                        if line.ends_with('\r') {
381                            line.pop();
382                        }
383                    }
384                    lines.push(line);
385                    self.lines_read += 1;
386                }
387                Err(e) => {
388                    self.finished = true;
389                    return Err(IoError::FileError(format!("Failed to read line: {}", e)));
390                }
391            }
392        }
393
394        if lines.is_empty() {
395            Ok(None)
396        } else {
397            Ok(Some(lines))
398        }
399    }
400}
401
402impl Stream for AsyncLineReader {
403    type Item = Result<Vec<String>>;
404
405    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
406        if self.finished {
407            return Poll::Ready(None);
408        }
409
410        let read_future = self.read_next_lines();
411        tokio::pin!(read_future);
412
413        match read_future.poll(cx) {
414            Poll::Ready(Ok(Some(lines))) => Poll::Ready(Some(Ok(lines))),
415            Poll::Ready(Ok(None)) => Poll::Ready(None),
416            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
417            Poll::Pending => Poll::Pending,
418        }
419    }
420}
421
422/// Async statistics for tracking async operations
423#[derive(Debug, Clone, Default)]
424pub struct AsyncStreamingStats {
425    /// Total bytes processed
426    pub bytes_processed: u64,
427    /// Total chunks processed
428    pub chunks_processed: usize,
429    /// Total lines processed (for line-based readers)
430    pub lines_processed: usize,
431    /// Processing time in milliseconds
432    pub processing_time_ms: f64,
433    /// Number of concurrent operations
434    pub concurrent_operations: usize,
435    /// Average processing speed in MB/s
436    pub avg_speed_mbps: f64,
437    /// Peak memory usage in bytes
438    pub peak_memory_usage: usize,
439}
440
441impl AsyncStreamingStats {
442    /// Create new async streaming statistics
443    pub fn new() -> Self {
444        Self::default()
445    }
446
447    /// Update statistics with chunk information
448    pub fn update_chunk(&mut self, bytes: u64, processing_time_ms: f64) {
449        self.bytes_processed += bytes;
450        self.chunks_processed += 1;
451        self.processing_time_ms += processing_time_ms;
452
453        if self.processing_time_ms > 0.0 {
454            let total_mb = self.bytes_processed as f64 / (1024.0 * 1024.0);
455            let total_seconds = self.processing_time_ms / 1000.0;
456            self.avg_speed_mbps = total_mb / total_seconds;
457        }
458    }
459
460    /// Update statistics with line information
461    pub fn update_lines(&mut self, lines: usize) {
462        self.lines_processed += lines;
463    }
464
465    /// Update concurrent operations count
466    pub fn update_concurrency(&mut self, count: usize) {
467        self.concurrent_operations = count;
468    }
469
470    /// Update peak memory usage
471    pub fn update_memory_usage(&mut self, usage: usize) {
472        self.peak_memory_usage = self.peak_memory_usage.max(usage);
473    }
474
475    /// Get a summary string of the async statistics
476    pub fn summary(&self) -> String {
477        format!(
478            "Async processed {} bytes in {} chunks ({} lines), {:.2} MB/s, {} concurrent ops, peak memory: {} KB",
479            self.bytes_processed,
480            self.chunks_processed,
481            self.lines_processed,
482            self.avg_speed_mbps,
483            self.concurrent_operations,
484            self.peak_memory_usage / 1024
485        )
486    }
487}
488
489/// Process a file asynchronously with concurrent chunk processing
490pub async fn process_file_async<P, F, Fut, T>(
491    path: P,
492    config: AsyncStreamingConfig,
493    processor: F,
494) -> Result<(Vec<T>, AsyncStreamingStats)>
495where
496    P: AsRef<Path>,
497    F: Fn(Vec<u8>, usize) -> Fut + Send + Sync + 'static,
498    Fut: std::future::Future<Output = Result<T>> + Send,
499    T: Send + 'static,
500{
501    let reader = AsyncChunkedReader::new(path, config.clone()).await?;
502    let mut stats = AsyncStreamingStats::new();
503    let mut results = Vec::new();
504
505    let start_time = std::time::Instant::now();
506
507    // Process chunks with controlled concurrency
508    let processor = std::sync::Arc::new(processor);
509    let mut chunk_stream = reader
510        .enumerate()
511        .map(|(chunk_id, chunk_result)| {
512            let processor = processor.clone();
513            async move {
514                match chunk_result {
515                    Ok(chunk) => {
516                        let chunk_start = std::time::Instant::now();
517                        let result = processor(chunk.clone(), chunk_id).await?;
518                        let processing_time = chunk_start.elapsed().as_secs_f64() * 1000.0;
519                        Ok((result, chunk.len(), processing_time))
520                    }
521                    Err(e) => Err(e),
522                }
523            }
524        })
525        .buffer_unordered(config.concurrency);
526
527    // Collect results
528    while let Some(result) = chunk_stream.next().await {
529        match result {
530            Ok((processed_result, bytes, processing_time)) => {
531                results.push(processed_result);
532                stats.update_chunk(bytes as u64, processing_time);
533                stats.update_concurrency(config.concurrency);
534            }
535            Err(e) => return Err(e),
536        }
537    }
538
539    stats.processing_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
540
541    Ok((results, stats))
542}
543
544/// Process a CSV file asynchronously with concurrent processing
545pub async fn process_csv_async<P, F, Fut, T>(
546    path: P,
547    config: AsyncStreamingConfig,
548    processor: F,
549) -> Result<(Vec<T>, AsyncStreamingStats)>
550where
551    P: AsRef<Path>,
552    F: Fn(Vec<String>, usize) -> Fut + Send + Sync + 'static,
553    Fut: std::future::Future<Output = Result<T>> + Send,
554    T: Send + 'static,
555{
556    let reader = AsyncLineReader::new(path, config.clone()).await?;
557    let mut stats = AsyncStreamingStats::new();
558    let mut results = Vec::new();
559
560    let start_time = std::time::Instant::now();
561
562    // Process line chunks with controlled concurrency
563    let processor = std::sync::Arc::new(processor);
564    let mut line_stream = reader
565        .enumerate()
566        .map(|(chunk_id, lines_result)| {
567            let processor = processor.clone();
568            async move {
569                match lines_result {
570                    Ok(lines) => {
571                        let chunk_start = std::time::Instant::now();
572                        let mut chunk_results = Vec::new();
573
574                        for (line_id, line) in lines.into_iter().enumerate() {
575                            let result = processor(vec![line], chunk_id * 1000 + line_id).await?;
576                            chunk_results.push(result);
577                        }
578
579                        let processing_time = chunk_start.elapsed().as_secs_f64() * 1000.0;
580                        Ok((chunk_results, processing_time))
581                    }
582                    Err(e) => Err(e),
583                }
584            }
585        })
586        .buffer_unordered(config.concurrency);
587
588    // Collect results
589    while let Some(result) = line_stream.next().await {
590        match result {
591            Ok((chunk_results, processing_time)) => {
592                let line_count = chunk_results.len();
593                results.extend(chunk_results);
594                stats.update_chunk(0, processing_time); // CSV doesn't track bytes easily
595                stats.update_lines(line_count);
596                stats.update_concurrency(config.concurrency);
597            }
598            Err(e) => return Err(e),
599        }
600    }
601
602    stats.processing_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
603
604    Ok((results, stats))
605}
606
607/// Create a cancellation token for async operations
608pub struct CancellationToken {
609    cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
610}
611
612impl CancellationToken {
613    /// Create a new cancellation token
614    pub fn new() -> Self {
615        Self {
616            cancelled: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
617        }
618    }
619
620    /// Cancel the operation
621    pub fn cancel(&self) {
622        self.cancelled
623            .store(true, std::sync::atomic::Ordering::SeqCst);
624    }
625
626    /// Check if the operation is cancelled
627    pub fn is_cancelled(&self) -> bool {
628        self.cancelled.load(std::sync::atomic::Ordering::SeqCst)
629    }
630
631    /// Create a clone of this token (for sharing across async tasks)
632    pub fn clone(&self) -> Self {
633        Self {
634            cancelled: self.cancelled.clone(),
635        }
636    }
637}
638
639impl Default for CancellationToken {
640    fn default() -> Self {
641        Self::new()
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use tempfile::tempdir;
649
650    #[tokio::test]
651    async fn test_async_chunked_reader() {
652        let temp_dir = tempdir().expect("Operation failed");
653        let file_path = temp_dir.path().join("test_async.txt");
654
655        // Create test data
656        let test_data = "0123456789".repeat(100); // 1000 bytes
657        std::fs::write(&file_path, &test_data).expect("Operation failed");
658
659        let config = AsyncStreamingConfig::new().chunk_size(100);
660        let mut reader = AsyncChunkedReader::new(&file_path, config)
661            .await
662            .expect("Operation failed");
663
664        let mut chunks = Vec::new();
665        while let Some(chunk_result) = reader.read_next_chunk().await.expect("Operation failed") {
666            chunks.push(chunk_result);
667        }
668
669        assert_eq!(chunks.len(), 10); // 1000 bytes / 100 bytes per chunk
670        for chunk in &chunks {
671            assert_eq!(chunk.len(), 100);
672        }
673    }
674
675    #[tokio::test]
676    async fn test_async_line_reader() {
677        let temp_dir = tempdir().expect("Operation failed");
678        let file_path = temp_dir.path().join("test_async_lines.txt");
679
680        // Create test data with lines
681        let lines: Vec<String> = (0..50).map(|i| format!("Line {}", i)).collect();
682        std::fs::write(&file_path, lines.join("\n")).expect("Operation failed");
683
684        let config = AsyncStreamingConfig::new().chunk_size(10); // 10 lines per chunk
685        let mut reader = AsyncLineReader::new(&file_path, config)
686            .await
687            .expect("Operation failed");
688
689        let mut chunks = Vec::new();
690        while let Some(lines_result) = reader.read_next_lines().await.expect("Operation failed") {
691            chunks.push(lines_result);
692        }
693
694        assert_eq!(chunks.len(), 5); // 50 lines / 10 lines per chunk
695        for chunk in &chunks {
696            assert_eq!(chunk.len(), 10);
697        }
698    }
699
700    #[tokio::test]
701    async fn test_async_config() {
702        let config = AsyncStreamingConfig::new()
703            .chunk_size(1024)
704            .buffer_size(4096)
705            .concurrency(8)
706            .timeout(5000);
707
708        assert_eq!(config.chunk_size, 1024);
709        assert_eq!(config.buffer_size, 4096);
710        assert_eq!(config.concurrency, 8);
711        assert_eq!(config.operation_timeout_ms, Some(5000));
712    }
713
714    #[tokio::test]
715    async fn test_cancellation_token() {
716        let token = CancellationToken::new();
717        assert!(!token.is_cancelled());
718
719        token.cancel();
720        assert!(token.is_cancelled());
721
722        let cloned_token = token.clone();
723        assert!(cloned_token.is_cancelled());
724    }
725}