Skip to main content

scirs2_io/distributed/
mod.rs

1//! Distributed I/O processing capabilities
2//!
3//! This module provides infrastructure for distributed processing of large datasets
4//! across multiple nodes or processes, enabling scalable I/O operations for
5//! terabyte-scale data processing.
6//!
7//! ## Features
8//!
9//! - **Distributed file reading**: Split large files across multiple workers
10//! - **Parallel writing**: Coordinate writes from multiple processes
11//! - **Data partitioning**: Automatic partitioning strategies for various formats
12//! - **Load balancing**: Dynamic work distribution based on node capabilities
13//! - **Fault tolerance**: Handle node failures and data recovery
14//! - **Progress tracking**: Monitor distributed operations
15//!
16//! ## Examples
17//!
18//! ```rust,no_run
19//! use scirs2_io::distributed::{DistributedReader, PartitionStrategy};
20//! use scirs2_core::ndarray::Array2;
21//!
22//! // Create a distributed reader for a large CSV file
23//! let reader = DistributedReader::new("large_dataset.csv")
24//!     .partition_strategy(PartitionStrategy::RowBased { chunk_size: 1_000_000 })
25//!     .num_workers(4);
26//!
27//! // Process chunks in parallel
28//! let results: Vec<i32> = reader.process_parallel(|chunk| {
29//!     // Process each chunk (calculate some statistic from the bytes)
30//!     // This is a simplified example - real implementation would parse CSV data
31//!     let sum: u32 = chunk.iter().map(|&b| b as u32).sum();
32//!     Ok((sum / chunk.len() as u32) as i32) // Return average byte value
33//! })?;
34//! # Ok::<(), scirs2_io::error::IoError>(())
35//! ```
36
37#![allow(dead_code)]
38#![allow(missing_docs)]
39#![allow(clippy::too_many_arguments)]
40
41/// Sharded array storage: partition large arrays across multiple files
42pub mod sharding;
43
44// Re-export sharding types for convenience
45pub use sharding::{
46    ElementType, HashSharding, RangeSharding, RoundRobinSharding, ShardConfig, ShardReader,
47    ShardWriter, ShardedArray, ShardedArrayMeta, ShardingStrategy, VirtualConcatenation,
48};
49
50use crate::error::{IoError, Result};
51use crate::thread_pool::ThreadPool;
52use scirs2_core::ndarray::Array2;
53use std::fs::File;
54use std::io::{Read, Seek, SeekFrom, Write};
55use std::path::{Path, PathBuf};
56use std::sync::{Arc, Mutex};
57use std::thread;
58
59/// Partition strategy for distributed processing
60#[derive(Clone)]
61pub enum PartitionStrategy {
62    /// Partition by rows (for tabular data)
63    RowBased { chunk_size: usize },
64    /// Partition by file size
65    SizeBased { chunk_size_bytes: usize },
66    /// Partition by blocks (for structured formats)
67    BlockBased { blocks_per_partition: usize },
68    /// Custom partitioning function
69    Custom(Arc<dyn Fn(usize) -> Vec<(usize, usize)> + Send + Sync>),
70}
71
72impl std::fmt::Debug for PartitionStrategy {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::RowBased { chunk_size } => f
76                .debug_struct("RowBased")
77                .field("chunk_size", chunk_size)
78                .finish(),
79            Self::SizeBased { chunk_size_bytes } => f
80                .debug_struct("SizeBased")
81                .field("chunk_size_bytes", chunk_size_bytes)
82                .finish(),
83            Self::BlockBased {
84                blocks_per_partition,
85            } => f
86                .debug_struct("BlockBased")
87                .field("blocks_per_partition", blocks_per_partition)
88                .finish(),
89            Self::Custom(_) => f
90                .debug_struct("Custom")
91                .field("function", &"<function>")
92                .finish(),
93        }
94    }
95}
96
97/// Worker status
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum WorkerStatus {
100    Idle,
101    Processing,
102    Completed,
103    Failed,
104}
105
106/// Worker information
107#[derive(Debug, Clone)]
108pub struct WorkerInfo {
109    /// Worker ID
110    pub id: usize,
111    /// Current status
112    pub status: WorkerStatus,
113    /// Progress (0.0 to 1.0)
114    pub progress: f64,
115    /// Items processed
116    pub items_processed: usize,
117    /// Error message if failed
118    pub error: Option<String>,
119}
120
121/// Distributed reader for parallel file processing
122pub struct DistributedReader {
123    file_path: PathBuf,
124    partition_strategy: PartitionStrategy,
125    num_workers: usize,
126    #[allow(dead_code)]
127    worker_pool: Option<ThreadPool>,
128    progress_callback: Option<Arc<dyn Fn(&[WorkerInfo]) + Send + Sync>>,
129}
130
131impl DistributedReader {
132    /// Create a new distributed reader
133    pub fn new<P: AsRef<Path>>(path: P) -> Self {
134        Self {
135            file_path: path.as_ref().to_path_buf(),
136            partition_strategy: PartitionStrategy::SizeBased {
137                chunk_size_bytes: 64 * 1024 * 1024,
138            }, // 64MB default
139            num_workers: num_cpus::get(),
140            worker_pool: None,
141            progress_callback: None,
142        }
143    }
144
145    /// Set partition strategy
146    pub fn partition_strategy(mut self, strategy: PartitionStrategy) -> Self {
147        self.partition_strategy = strategy;
148        self
149    }
150
151    /// Set number of workers
152    pub fn num_workers(mut self, num_workers: usize) -> Self {
153        self.num_workers = num_workers;
154        self
155    }
156
157    /// Set progress callback
158    pub fn progress_callback<F>(mut self, callback: F) -> Self
159    where
160        F: Fn(&[WorkerInfo]) + Send + Sync + 'static,
161    {
162        self.progress_callback = Some(Arc::new(callback));
163        self
164    }
165
166    /// Get file size
167    fn get_file_size(&self) -> Result<usize> {
168        let metadata = std::fs::metadata(&self.file_path)
169            .map_err(|_| IoError::FileNotFound(self.file_path.to_string_lossy().to_string()))?;
170        Ok(metadata.len() as usize)
171    }
172
173    /// Create partitions based on strategy
174    fn create_partitions(&self) -> Result<Vec<(usize, usize)>> {
175        let file_size = self.get_file_size()?;
176
177        match &self.partition_strategy {
178            PartitionStrategy::SizeBased { chunk_size_bytes } => {
179                let mut partitions = Vec::new();
180                let mut offset = 0;
181
182                while offset < file_size {
183                    let end = (offset + chunk_size_bytes).min(file_size);
184                    partitions.push((offset, end - offset));
185                    offset = end;
186                }
187
188                Ok(partitions)
189            }
190            PartitionStrategy::RowBased { chunk_size } => {
191                // For row-based partitioning, we need to scan the file
192                // This is a simplified implementation
193                let total_rows = self.estimate_row_count()?;
194                let mut partitions = Vec::new();
195                let mut row_offset = 0;
196
197                while row_offset < total_rows {
198                    let rows = (*chunk_size).min(total_rows - row_offset);
199                    partitions.push((row_offset, rows));
200                    row_offset += rows;
201                }
202
203                Ok(partitions)
204            }
205            PartitionStrategy::BlockBased {
206                blocks_per_partition,
207            } => {
208                // For block-based formats
209                let block_size = 4096; // Example block size
210                let total_blocks = (file_size + block_size - 1) / block_size;
211                let mut partitions = Vec::new();
212                let mut block_offset = 0;
213
214                while block_offset < total_blocks {
215                    let blocks = (*blocks_per_partition).min(total_blocks - block_offset);
216                    partitions.push((block_offset * block_size, blocks * block_size));
217                    block_offset += blocks;
218                }
219
220                Ok(partitions)
221            }
222            PartitionStrategy::Custom(f) => Ok(f(file_size)),
223        }
224    }
225
226    /// Estimate row count for row-based partitioning
227    fn estimate_row_count(&self) -> Result<usize> {
228        // Simplified: sample first few KB and estimate
229        let mut file = File::open(&self.file_path)
230            .map_err(|_| IoError::FileNotFound(self.file_path.to_string_lossy().to_string()))?;
231
232        let mut buffer = vec![0u8; 8192];
233        let bytes_read = file
234            .read(&mut buffer)
235            .map_err(|e| IoError::ParseError(format!("Failed to read sample: {e}")))?;
236
237        let newlines = buffer[..bytes_read].iter().filter(|&&b| b == b'\n').count();
238        if newlines == 0 {
239            return Ok(1);
240        }
241
242        let file_size = self.get_file_size()?;
243        let estimated_rows = (file_size as f64 / bytes_read as f64 * newlines as f64) as usize;
244
245        Ok(estimated_rows)
246    }
247
248    /// Process file in parallel with enhanced load balancing and error recovery
249    pub fn process_parallel<T, F>(&self, processor: F) -> Result<Vec<T>>
250    where
251        T: Send + 'static + std::cmp::Ord,
252        F: Fn(Vec<u8>) -> Result<T> + Send + Sync + 'static,
253    {
254        let partitions = self.create_partitions()?;
255        let num_partitions = partitions.len();
256
257        // Adaptive load balancing: adjust partition size based on system resources
258        let available_workers = std::cmp::min(self.num_workers, num_partitions);
259        let cpu_count = num_cpus::get();
260        let optimal_workers = std::cmp::min(available_workers, cpu_count * 2); // Don't over-subscribe
261
262        println!(
263            "Processing {num_partitions} partitions with {optimal_workers} workers (CPU cores: {cpu_count})"
264        );
265
266        // Create worker info tracking
267        let worker_infos = Arc::new(Mutex::new(
268            (0..num_partitions)
269                .map(|i| WorkerInfo {
270                    id: i,
271                    status: WorkerStatus::Idle,
272                    progress: 0.0,
273                    items_processed: 0,
274                    error: None,
275                })
276                .collect::<Vec<_>>(),
277        ));
278
279        // Process partitions in parallel
280        let results = Arc::new(Mutex::new(Vec::with_capacity(num_partitions)));
281        let processor = Arc::new(processor);
282        let file_path = self.file_path.clone();
283        let progress_callback = self.progress_callback.clone();
284
285        // Use thread pool or spawn threads
286        let handles: Vec<_> = partitions
287            .into_iter()
288            .enumerate()
289            .map(|(idx, (offset, size))| {
290                let file_path = file_path.clone();
291                let processor = processor.clone();
292                let results = results.clone();
293                let worker_infos = worker_infos.clone();
294                let progress_callback = progress_callback.clone();
295
296                thread::spawn(move || {
297                    // Update status
298                    {
299                        let mut infos = worker_infos.lock().expect("Operation failed");
300                        infos[idx].status = WorkerStatus::Processing;
301                    }
302
303                    // Read partition
304                    let partition_result = (|| -> Result<T> {
305                        let mut file = File::open(&file_path).map_err(|_| {
306                            IoError::FileNotFound(file_path.to_string_lossy().to_string())
307                        })?;
308
309                        file.seek(SeekFrom::Start(offset as u64))
310                            .map_err(|e| IoError::ParseError(format!("Failed to seek: {e}")))?;
311
312                        let mut buffer = vec![0u8; size];
313                        file.read_exact(&mut buffer).map_err(|e| {
314                            IoError::ParseError(format!("Failed to read partition: {e}"))
315                        })?;
316
317                        processor(buffer)
318                    })();
319
320                    // Update status and store result
321                    match partition_result {
322                        Ok(result) => {
323                            let mut infos = worker_infos.lock().expect("Operation failed");
324                            infos[idx].status = WorkerStatus::Completed;
325                            infos[idx].progress = 1.0;
326                            infos[idx].items_processed = 1;
327                            drop(infos);
328
329                            let mut results_guard = results.lock().expect("Operation failed");
330                            results_guard.push((idx, Ok(result)));
331                        }
332                        Err(e) => {
333                            let mut infos = worker_infos.lock().expect("Operation failed");
334                            infos[idx].status = WorkerStatus::Failed;
335                            infos[idx].error = Some(e.to_string());
336                            drop(infos);
337
338                            let mut results_guard = results.lock().expect("Operation failed");
339                            results_guard.push((idx, Err(e)));
340                        }
341                    }
342
343                    // Call progress callback
344                    if let Some(callback) = &progress_callback {
345                        let infos = worker_infos.lock().expect("Operation failed");
346                        callback(&infos);
347                    }
348                })
349            })
350            .collect();
351
352        // Wait for all workers
353        for handle in handles {
354            handle
355                .join()
356                .map_err(|_| IoError::ParseError("Worker thread panicked".to_string()))?;
357        }
358
359        // Sort results by partition index and extract values
360        let mut results_guard = results.lock().expect("Operation failed");
361        results_guard.sort_by_key(|(idx_, _)| *idx_);
362
363        // Drain the results to own them, avoiding cloning issues
364        let sorted_results: Vec<_> = results_guard.drain(..).collect();
365        drop(results_guard);
366
367        // Extract the actual results
368        sorted_results
369            .into_iter()
370            .map(|(_, result)| result)
371            .collect()
372    }
373}
374
375/// Distributed writer for parallel file writing
376pub struct DistributedWriter {
377    output_dir: PathBuf,
378    num_partitions: usize,
379    partition_naming: Arc<dyn Fn(usize) -> String + Send + Sync>,
380    merge_strategy: MergeStrategy,
381}
382
383/// Strategy for merging distributed write outputs
384#[derive(Clone)]
385pub enum MergeStrategy {
386    /// No merging - keep separate files
387    None,
388    /// Concatenate files in order
389    Concatenate { output_file: PathBuf },
390    /// Custom merge function
391    Custom(Arc<dyn Fn(&[PathBuf], &Path) -> Result<()> + Send + Sync>),
392}
393
394impl std::fmt::Debug for MergeStrategy {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        match self {
397            MergeStrategy::None => write!(f, "MergeStrategy::None"),
398            MergeStrategy::Concatenate { output_file } => f
399                .debug_struct("MergeStrategy::Concatenate")
400                .field("output_file", output_file)
401                .finish(),
402            MergeStrategy::Custom(_) => write!(f, "MergeStrategy::Custom(<function>)"),
403        }
404    }
405}
406
407impl DistributedWriter {
408    /// Create a new distributed writer
409    pub fn new<P: AsRef<Path>>(output_dir: P) -> Self {
410        Self {
411            output_dir: output_dir.as_ref().to_path_buf(),
412            num_partitions: num_cpus::get(),
413            partition_naming: Arc::new(|idx| format!("partition_{idx:04}.dat")),
414            merge_strategy: MergeStrategy::None,
415        }
416    }
417
418    /// Set number of partitions
419    pub fn num_partitions(mut self, num: usize) -> Self {
420        self.num_partitions = num;
421        self
422    }
423
424    /// Set partition naming function
425    pub fn partition_naming<F>(mut self, naming: F) -> Self
426    where
427        F: Fn(usize) -> String + Send + Sync + 'static,
428    {
429        self.partition_naming = Arc::new(naming);
430        self
431    }
432
433    /// Set merge strategy
434    pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
435        self.merge_strategy = strategy;
436        self
437    }
438
439    /// Write data in parallel
440    pub fn write_parallel<T, F>(&self, data: Vec<T>, writer: F) -> Result<Vec<PathBuf>>
441    where
442        T: Send + 'static + Clone,
443        F: Fn(&T, &mut File) -> Result<()> + Send + Sync + 'static,
444    {
445        // Create output directory
446        std::fs::create_dir_all(&self.output_dir)
447            .map_err(|e| IoError::FileError(format!("Failed to create output directory: {e}")))?;
448
449        // Partition data
450        let chunk_size = (data.len() + self.num_partitions - 1) / self.num_partitions;
451        let chunks: Vec<_> = data
452            .into_iter()
453            .collect::<Vec<_>>()
454            .chunks(chunk_size)
455            .map(|chunk| chunk.to_vec())
456            .collect();
457
458        let writer = Arc::new(writer);
459        let output_dir = self.output_dir.clone();
460        let partition_naming = self.partition_naming.clone();
461
462        // Write partitions in parallel
463        let handles: Vec<_> = chunks
464            .into_iter()
465            .enumerate()
466            .map(|(idx, chunk)| {
467                let writer = writer.clone();
468                let output_dir = output_dir.clone();
469                let partition_naming = partition_naming.clone();
470
471                thread::spawn(move || -> Result<PathBuf> {
472                    let filename = partition_naming(idx);
473                    let filepath = output_dir.join(&filename);
474
475                    let mut file = File::create(&filepath).map_err(|e| {
476                        IoError::FileError(format!("Failed to create partition file: {e}"))
477                    })?;
478
479                    for item in chunk {
480                        writer(&item, &mut file)?;
481                    }
482
483                    file.sync_all()
484                        .map_err(|e| IoError::FileError(format!("Failed to sync file: {e}")))?;
485
486                    Ok(filepath)
487                })
488            })
489            .collect();
490
491        // Collect results
492        let mut partition_files = Vec::new();
493        for handle in handles {
494            let filepath = handle
495                .join()
496                .map_err(|_| IoError::FileError("Writer thread panicked".to_string()))??;
497            partition_files.push(filepath);
498        }
499
500        // Apply merge strategy
501        match &self.merge_strategy {
502            MergeStrategy::None => Ok(partition_files),
503            MergeStrategy::Concatenate { output_file } => {
504                self.merge_files(&partition_files, output_file)?;
505                Ok(vec![output_file.clone()])
506            }
507            MergeStrategy::Custom(merger) => {
508                let merged_file = self.output_dir.join("merged.dat");
509                merger(&partition_files, &merged_file)?;
510                Ok(vec![merged_file])
511            }
512        }
513    }
514
515    /// Merge partition files
516    fn merge_files(&self, partitions: &[PathBuf], output: &Path) -> Result<()> {
517        let mut output_file = File::create(output)
518            .map_err(|e| IoError::FileError(format!("Failed to create merge output: {e}")))?;
519
520        for partition in partitions {
521            let mut input = File::open(partition)
522                .map_err(|_| IoError::FileNotFound(partition.to_string_lossy().to_string()))?;
523
524            std::io::copy(&mut input, &mut output_file)
525                .map_err(|e| IoError::FileError(format!("Failed to copy partition: {e}")))?;
526        }
527
528        output_file
529            .sync_all()
530            .map_err(|e| IoError::FileError(format!("Failed to sync merged file: {e}")))?;
531
532        // Optionally delete partition files
533        for partition in partitions {
534            let _ = std::fs::remove_file(partition);
535        }
536
537        Ok(())
538    }
539}
540
541/// Distributed array operations
542pub struct DistributedArray {
543    partitions: Vec<ArrayPartition>,
544    shape: Vec<usize>,
545    #[allow(dead_code)]
546    distribution: Distribution,
547}
548
549/// Array partition
550struct ArrayPartition {
551    data: Array2<f64>,
552    global_offset: Vec<usize>,
553    node_id: usize,
554}
555
556/// Distribution strategy for arrays
557#[derive(Debug, Clone)]
558pub enum Distribution {
559    /// Block distribution
560    Block { block_size: Vec<usize> },
561    /// Cyclic distribution
562    Cyclic { cycle_size: usize },
563    /// Block-cyclic distribution
564    BlockCyclic {
565        block_size: usize,
566        cycle_size: usize,
567    },
568}
569
570impl DistributedArray {
571    /// Create a new distributed array
572    pub fn new(shape: Vec<usize>, distribution: Distribution) -> Self {
573        Self {
574            partitions: Vec::new(),
575            shape,
576            distribution,
577        }
578    }
579
580    /// Add a partition
581    pub fn add_partition(&mut self, data: Array2<f64>, offset: Vec<usize>, nodeid: usize) {
582        self.partitions.push(ArrayPartition {
583            data,
584            global_offset: offset,
585            node_id: nodeid,
586        });
587    }
588
589    /// Get total shape
590    pub fn shape(&self) -> &[usize] {
591        &self.shape
592    }
593
594    /// Get local partition for a node
595    pub fn get_local_partition(&self, nodeid: usize) -> Option<&Array2<f64>> {
596        self.partitions
597            .iter()
598            .find(|p| p.node_id == nodeid)
599            .map(|p| &p.data)
600    }
601
602    /// Gather all partitions into a single array
603    pub fn gather(&self) -> Result<Array2<f64>> {
604        if self.shape.len() != 2 {
605            return Err(IoError::ParseError(
606                "Only 2D arrays supported for gather".to_string(),
607            ));
608        }
609
610        let mut result = Array2::zeros((self.shape[0], self.shape[1]));
611
612        for partition in &self.partitions {
613            let (rows, cols) = partition.data.dim();
614            let row_start = partition.global_offset[0];
615            let col_start = partition.global_offset[1];
616
617            for i in 0..rows {
618                for j in 0..cols {
619                    result[[row_start + i, col_start + j]] = partition.data[[i, j]];
620                }
621            }
622        }
623
624        Ok(result)
625    }
626
627    /// Scatter a single array into distributed partitions
628    pub fn scatter(
629        array: &Array2<f64>,
630        distribution: Distribution,
631        num_nodes: usize,
632    ) -> Result<Self> {
633        let shape = vec![array.nrows(), array.ncols()];
634        let mut distributed = Self::new(shape.clone(), distribution.clone());
635
636        match distribution {
637            Distribution::Block { block_size: _ } => {
638                let rows_per_node = (array.nrows() + num_nodes - 1) / num_nodes;
639
640                for node_id in 0..num_nodes {
641                    let row_start = node_id * rows_per_node;
642                    let row_end = ((node_id + 1) * rows_per_node).min(array.nrows());
643
644                    if row_start < array.nrows() {
645                        let partition = array.slice(s![row_start..row_end, ..]).to_owned();
646                        distributed.add_partition(partition, vec![row_start, 0], node_id);
647                    }
648                }
649            }
650            _ => {
651                return Err(IoError::ParseError(
652                    "Unsupported distribution for scatter".to_string(),
653                ));
654            }
655        }
656
657        Ok(distributed)
658    }
659}
660
661/// Distributed file system abstraction
662pub trait DistributedFileSystem: Send + Sync {
663    /// Open a file for reading
664    fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>>;
665
666    /// Create a file for writing
667    fn create_write(&self, path: &Path) -> Result<Box<dyn Write + Send>>;
668
669    /// List files in a directory
670    fn list_dir(&self, path: &Path) -> Result<Vec<PathBuf>>;
671
672    /// Get file metadata
673    fn metadata(&self, path: &Path) -> Result<FileMetadata>;
674
675    /// Check if path exists
676    fn exists(&self, path: &Path) -> bool;
677}
678
679/// File metadata
680#[derive(Debug, Clone)]
681pub struct FileMetadata {
682    pub size: u64,
683    pub modified: std::time::SystemTime,
684    pub is_dir: bool,
685}
686
687/// Local file system implementation
688pub struct LocalFileSystem;
689
690impl DistributedFileSystem for LocalFileSystem {
691    fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>> {
692        let file = File::open(path)
693            .map_err(|_| IoError::FileNotFound(path.to_string_lossy().to_string()))?;
694        Ok(Box::new(file))
695    }
696
697    fn create_write(&self, path: &Path) -> Result<Box<dyn Write + Send>> {
698        let file = File::create(path)
699            .map_err(|e| IoError::FileError(format!("Failed to create file: {e}")))?;
700        Ok(Box::new(file))
701    }
702
703    fn list_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
704        let entries = std::fs::read_dir(path)
705            .map_err(|e| IoError::ParseError(format!("Failed to read directory: {e}")))?;
706
707        let mut paths = Vec::new();
708        for entry in entries {
709            let entry =
710                entry.map_err(|e| IoError::ParseError(format!("Failed to read entry: {e}")))?;
711            paths.push(entry.path());
712        }
713
714        Ok(paths)
715    }
716
717    fn metadata(&self, path: &Path) -> Result<FileMetadata> {
718        let meta = std::fs::metadata(path)
719            .map_err(|_| IoError::FileNotFound(path.to_string_lossy().to_string()))?;
720
721        Ok(FileMetadata {
722            size: meta.len(),
723            modified: meta
724                .modified()
725                .map_err(|e| IoError::ParseError(format!("Failed to get modified time: {e}")))?,
726            is_dir: meta.is_dir(),
727        })
728    }
729
730    fn exists(&self, path: &Path) -> bool {
731        path.exists()
732    }
733}
734
735// Helper for s! macro
736use scirs2_core::ndarray::s;
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use tempfile::TempDir;
742
743    #[test]
744    fn test_partition_strategies() {
745        let temp_dir = TempDir::new().expect("Operation failed");
746        let temp_file = temp_dir.path().join("test.dat");
747        std::fs::write(&temp_file, vec![0u8; 10000]).expect("Operation failed");
748
749        let reader =
750            DistributedReader::new(&temp_file).partition_strategy(PartitionStrategy::SizeBased {
751                chunk_size_bytes: 1000,
752            });
753
754        let partitions = reader.create_partitions().expect("Operation failed");
755        assert_eq!(partitions.len(), 10);
756
757        for (_offset, size) in &partitions {
758            assert_eq!(*size, 1000);
759        }
760    }
761
762    #[test]
763    fn test_distributed_array() {
764        let array = Array2::from_shape_fn((100, 50), |(i, j)| (i * 50 + j) as f64);
765
766        let distributed = DistributedArray::scatter(
767            &array,
768            Distribution::Block {
769                block_size: vec![25, 50],
770            },
771            4,
772        )
773        .expect("Operation failed");
774
775        assert_eq!(distributed.partitions.len(), 4);
776
777        let gathered = distributed.gather().expect("Operation failed");
778        assert_eq!(array, gathered);
779    }
780
781    #[test]
782    fn test_distributed_writer() {
783        let temp_dir = TempDir::new().expect("Operation failed");
784
785        let data: Vec<i32> = (0..100).collect();
786        let writer = DistributedWriter::new(temp_dir.path()).num_partitions(4);
787
788        let files = writer
789            .write_parallel(data, |&value, file| {
790                writeln!(file, "{value}")
791                    .map_err(|e| IoError::FileError(format!("Failed to write: {e}")))
792            })
793            .expect("Operation failed");
794
795        assert_eq!(files.len(), 4);
796
797        // Verify all files exist
798        for file in &files {
799            assert!(file.exists());
800        }
801    }
802}