1#![allow(dead_code)]
38#![allow(missing_docs)]
39#![allow(clippy::too_many_arguments)]
40
41pub mod sharding;
43
44pub 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#[derive(Clone)]
61pub enum PartitionStrategy {
62 RowBased { chunk_size: usize },
64 SizeBased { chunk_size_bytes: usize },
66 BlockBased { blocks_per_partition: usize },
68 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum WorkerStatus {
100 Idle,
101 Processing,
102 Completed,
103 Failed,
104}
105
106#[derive(Debug, Clone)]
108pub struct WorkerInfo {
109 pub id: usize,
111 pub status: WorkerStatus,
113 pub progress: f64,
115 pub items_processed: usize,
117 pub error: Option<String>,
119}
120
121pub 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 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 }, num_workers: num_cpus::get(),
140 worker_pool: None,
141 progress_callback: None,
142 }
143 }
144
145 pub fn partition_strategy(mut self, strategy: PartitionStrategy) -> Self {
147 self.partition_strategy = strategy;
148 self
149 }
150
151 pub fn num_workers(mut self, num_workers: usize) -> Self {
153 self.num_workers = num_workers;
154 self
155 }
156
157 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 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 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 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 let block_size = 4096; 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 fn estimate_row_count(&self) -> Result<usize> {
228 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 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 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); println!(
263 "Processing {num_partitions} partitions with {optimal_workers} workers (CPU cores: {cpu_count})"
264 );
265
266 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 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 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 {
299 let mut infos = worker_infos.lock().expect("Operation failed");
300 infos[idx].status = WorkerStatus::Processing;
301 }
302
303 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 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 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 for handle in handles {
354 handle
355 .join()
356 .map_err(|_| IoError::ParseError("Worker thread panicked".to_string()))?;
357 }
358
359 let mut results_guard = results.lock().expect("Operation failed");
361 results_guard.sort_by_key(|(idx_, _)| *idx_);
362
363 let sorted_results: Vec<_> = results_guard.drain(..).collect();
365 drop(results_guard);
366
367 sorted_results
369 .into_iter()
370 .map(|(_, result)| result)
371 .collect()
372 }
373}
374
375pub 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#[derive(Clone)]
385pub enum MergeStrategy {
386 None,
388 Concatenate { output_file: PathBuf },
390 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 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 pub fn num_partitions(mut self, num: usize) -> Self {
420 self.num_partitions = num;
421 self
422 }
423
424 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 pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
435 self.merge_strategy = strategy;
436 self
437 }
438
439 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 std::fs::create_dir_all(&self.output_dir)
447 .map_err(|e| IoError::FileError(format!("Failed to create output directory: {e}")))?;
448
449 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 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 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 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 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 for partition in partitions {
534 let _ = std::fs::remove_file(partition);
535 }
536
537 Ok(())
538 }
539}
540
541pub struct DistributedArray {
543 partitions: Vec<ArrayPartition>,
544 shape: Vec<usize>,
545 #[allow(dead_code)]
546 distribution: Distribution,
547}
548
549struct ArrayPartition {
551 data: Array2<f64>,
552 global_offset: Vec<usize>,
553 node_id: usize,
554}
555
556#[derive(Debug, Clone)]
558pub enum Distribution {
559 Block { block_size: Vec<usize> },
561 Cyclic { cycle_size: usize },
563 BlockCyclic {
565 block_size: usize,
566 cycle_size: usize,
567 },
568}
569
570impl DistributedArray {
571 pub fn new(shape: Vec<usize>, distribution: Distribution) -> Self {
573 Self {
574 partitions: Vec::new(),
575 shape,
576 distribution,
577 }
578 }
579
580 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 pub fn shape(&self) -> &[usize] {
591 &self.shape
592 }
593
594 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 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 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
661pub trait DistributedFileSystem: Send + Sync {
663 fn open_read(&self, path: &Path) -> Result<Box<dyn Read + Send>>;
665
666 fn create_write(&self, path: &Path) -> Result<Box<dyn Write + Send>>;
668
669 fn list_dir(&self, path: &Path) -> Result<Vec<PathBuf>>;
671
672 fn metadata(&self, path: &Path) -> Result<FileMetadata>;
674
675 fn exists(&self, path: &Path) -> bool;
677}
678
679#[derive(Debug, Clone)]
681pub struct FileMetadata {
682 pub size: u64,
683 pub modified: std::time::SystemTime,
684 pub is_dir: bool,
685}
686
687pub 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
735use 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 for file in &files {
799 assert!(file.exists());
800 }
801 }
802}