1#![allow(dead_code)]
12#![allow(missing_docs)]
13
14use crate::error::{IoError, Result};
15use serde::{Deserialize, Serialize};
16use std::collections::hash_map::DefaultHasher;
17use std::hash::{Hash, Hasher};
18use std::io::{Read, Write};
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, Mutex};
21use std::thread;
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum ShardingStrategy {
30 RoundRobin,
32 Hash,
34 Range,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ShardConfig {
41 pub shard_count: usize,
43 pub shard_size: usize,
45 pub strategy: ShardingStrategy,
47 pub base_dir: PathBuf,
49 pub prefix: String,
51}
52
53impl ShardConfig {
54 pub fn new<P: AsRef<Path>>(base_dir: P, shard_count: usize) -> Self {
56 Self {
57 shard_count,
58 shard_size: 0,
59 strategy: ShardingStrategy::Range,
60 base_dir: base_dir.as_ref().to_path_buf(),
61 prefix: "shard".to_string(),
62 }
63 }
64
65 pub fn with_strategy(mut self, strategy: ShardingStrategy) -> Self {
67 self.strategy = strategy;
68 self
69 }
70
71 pub fn with_shard_size(mut self, size: usize) -> Self {
73 self.shard_size = size;
74 self
75 }
76
77 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
79 self.prefix = prefix.into();
80 self
81 }
82
83 pub fn shard_path(&self, index: usize) -> PathBuf {
85 self.base_dir
86 .join(format!("{}_{:04}.bin", self.prefix, index))
87 }
88
89 pub fn meta_path(&self) -> PathBuf {
91 self.base_dir.join(format!("{}_meta.json", self.prefix))
92 }
93}
94
95pub struct RoundRobinSharding;
101
102impl RoundRobinSharding {
103 pub fn shard_for(element_index: usize, shard_count: usize) -> usize {
105 if shard_count == 0 {
106 return 0;
107 }
108 element_index % shard_count
109 }
110}
111
112pub struct HashSharding;
114
115impl HashSharding {
116 pub fn shard_for(element_index: usize, shard_count: usize) -> usize {
118 if shard_count == 0 {
119 return 0;
120 }
121 let mut h = DefaultHasher::new();
122 element_index.hash(&mut h);
123 (h.finish() as usize) % shard_count
124 }
125}
126
127pub struct RangeSharding;
129
130impl RangeSharding {
131 pub fn shard_for(element_index: usize, shard_count: usize, total: usize) -> usize {
134 if shard_count == 0 || total == 0 {
135 return 0;
136 }
137 let block = (total + shard_count - 1) / shard_count;
138 (element_index / block).min(shard_count - 1)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ShardedArrayMeta {
149 pub total_elements: usize,
151 pub shard_count: usize,
153 pub shard_sizes: Vec<usize>,
155 pub strategy: ShardingStrategy,
157 pub element_bits: u8,
159 pub prefix: String,
161}
162
163pub struct ShardedArray {
172 config: ShardConfig,
173 meta: Option<ShardedArrayMeta>,
174}
175
176impl ShardedArray {
177 pub fn new(config: ShardConfig) -> Self {
179 Self { config, meta: None }
180 }
181
182 pub fn write(&mut self, data: &[f64]) -> Result<ShardedArrayMeta> {
186 std::fs::create_dir_all(&self.config.base_dir)
187 .map_err(|e| IoError::FileError(format!("Cannot create shard dir: {e}")))?;
188
189 let shard_count = self.config.shard_count;
190 let total = data.len();
191
192 let mut buckets: Vec<Vec<f64>> = vec![Vec::new(); shard_count];
194 for (i, &v) in data.iter().enumerate() {
195 let shard_idx = match self.config.strategy {
196 ShardingStrategy::RoundRobin => RoundRobinSharding::shard_for(i, shard_count),
197 ShardingStrategy::Hash => HashSharding::shard_for(i, shard_count),
198 ShardingStrategy::Range => RangeSharding::shard_for(i, shard_count, total),
199 };
200 buckets[shard_idx].push(v);
201 }
202
203 let mut shard_sizes = Vec::with_capacity(shard_count);
204 for (idx, bucket) in buckets.iter().enumerate() {
205 let path = self.config.shard_path(idx);
206 let mut f = std::fs::File::create(&path)
207 .map_err(|e| IoError::FileError(format!("Cannot create shard {idx}: {e}")))?;
208 for &v in bucket {
209 f.write_all(&v.to_le_bytes())
210 .map_err(|e| IoError::FileError(format!("Shard write error: {e}")))?;
211 }
212 shard_sizes.push(bucket.len());
213 }
214
215 let meta = ShardedArrayMeta {
216 total_elements: total,
217 shard_count,
218 shard_sizes,
219 strategy: self.config.strategy.clone(),
220 element_bits: 64,
221 prefix: self.config.prefix.clone(),
222 };
223
224 let meta_json = serde_json::to_string_pretty(&meta)
226 .map_err(|e| IoError::SerializationError(format!("{e}")))?;
227 let mut mf = std::fs::File::create(self.config.meta_path())
228 .map_err(|e| IoError::FileError(format!("Cannot write meta: {e}")))?;
229 mf.write_all(meta_json.as_bytes())
230 .map_err(|e| IoError::FileError(format!("Meta write error: {e}")))?;
231
232 self.meta = Some(meta.clone());
233 Ok(meta)
234 }
235
236 pub fn read(&self) -> Result<Vec<f64>> {
241 let meta = self.load_meta()?;
242 let shard_count = meta.shard_count;
243 let mut shards: Vec<Vec<f64>> = Vec::with_capacity(shard_count);
244 for idx in 0..shard_count {
245 let path = self.config.shard_path(idx);
246 let mut f = std::fs::File::open(&path)
247 .map_err(|_| IoError::NotFound(format!("Shard {idx} not found")))?;
248 let mut buf = Vec::new();
249 f.read_to_end(&mut buf)
250 .map_err(|e| IoError::FileError(format!("Shard read error: {e}")))?;
251 let values: Vec<f64> = buf
252 .chunks_exact(8)
253 .map(|b| {
254 let arr: [u8; 8] = b.try_into().unwrap_or([0u8; 8]);
255 f64::from_le_bytes(arr)
256 })
257 .collect();
258 shards.push(values);
259 }
260
261 let total = meta.total_elements;
262 match meta.strategy {
263 ShardingStrategy::Range => {
264 let result: Vec<f64> = shards.into_iter().flatten().collect();
266 Ok(result)
267 }
268 ShardingStrategy::RoundRobin => {
269 let mut result = vec![0.0f64; total];
270 let mut shard_cursors = vec![0usize; shard_count];
271 for i in 0..total {
272 let s = RoundRobinSharding::shard_for(i, shard_count);
273 let cursor = shard_cursors[s];
274 if cursor < shards[s].len() {
275 result[i] = shards[s][cursor];
276 shard_cursors[s] += 1;
277 }
278 }
279 Ok(result)
280 }
281 ShardingStrategy::Hash => {
282 let mut result = vec![0.0f64; total];
283 let mut shard_cursors = vec![0usize; shard_count];
284 for i in 0..total {
285 let s = HashSharding::shard_for(i, shard_count);
286 let cursor = shard_cursors[s];
287 if cursor < shards[s].len() {
288 result[i] = shards[s][cursor];
289 shard_cursors[s] += 1;
290 }
291 }
292 Ok(result)
293 }
294 }
295 }
296
297 fn load_meta(&self) -> Result<ShardedArrayMeta> {
298 if let Some(ref m) = self.meta {
299 return Ok(m.clone());
300 }
301 let path = self.config.meta_path();
302 let mut f = std::fs::File::open(&path)
303 .map_err(|_| IoError::NotFound("Shard metadata not found".to_string()))?;
304 let mut buf = String::new();
305 f.read_to_string(&mut buf)
306 .map_err(|e| IoError::FileError(format!("Cannot read meta: {e}")))?;
307 serde_json::from_str(&buf).map_err(|e| IoError::ParseError(format!("Bad meta: {e}")))
308 }
309}
310
311pub struct ShardReader {
317 config: ShardConfig,
318 num_threads: usize,
319}
320
321impl ShardReader {
322 pub fn new(config: ShardConfig) -> Self {
324 let cores = num_cpus::get().max(1);
325 Self {
326 config,
327 num_threads: cores,
328 }
329 }
330
331 pub fn with_threads(mut self, n: usize) -> Self {
333 self.num_threads = n.max(1);
334 self
335 }
336
337 pub fn read_all(&self) -> Result<Vec<f64>> {
339 let shard_count = self.config.shard_count;
340 let results: Arc<Mutex<Vec<(usize, Result<Vec<f64>>)>>> =
341 Arc::new(Mutex::new(Vec::with_capacity(shard_count)));
342
343 let handles: Vec<_> = (0..shard_count)
344 .map(|idx| {
345 let path = self.config.shard_path(idx);
346 let results = results.clone();
347 thread::spawn(move || {
348 let data = read_shard_raw(&path);
349 let mut guard = results.lock().expect("lock poisoned");
350 guard.push((idx, data));
351 })
352 })
353 .collect();
354
355 for h in handles {
356 h.join()
357 .map_err(|_| IoError::FileError("Shard reader thread panicked".to_string()))?;
358 }
359
360 let mut guard = results.lock().expect("lock poisoned");
361 guard.sort_by_key(|(idx, _)| *idx);
362 let mut merged = Vec::new();
363 for (_, result) in guard.drain(..) {
364 let data = result?;
365 merged.extend(data);
366 }
367 Ok(merged)
368 }
369
370 pub fn read_shard(&self, index: usize) -> Result<Vec<f64>> {
372 read_shard_raw(&self.config.shard_path(index))
373 }
374}
375
376fn read_shard_raw(path: &Path) -> Result<Vec<f64>> {
377 let mut f = std::fs::File::open(path)
378 .map_err(|_| IoError::NotFound(format!("Shard not found: {}", path.display())))?;
379 let mut buf = Vec::new();
380 f.read_to_end(&mut buf)
381 .map_err(|e| IoError::FileError(format!("Shard read error: {e}")))?;
382 Ok(buf
383 .chunks_exact(8)
384 .map(|b| {
385 let arr: [u8; 8] = b.try_into().unwrap_or([0u8; 8]);
386 f64::from_le_bytes(arr)
387 })
388 .collect())
389}
390
391pub struct ShardWriter {
393 config: ShardConfig,
394}
395
396impl ShardWriter {
397 pub fn new(config: ShardConfig) -> Self {
399 Self { config }
400 }
401
402 pub fn write(&self, data: &[f64]) -> Result<Vec<PathBuf>> {
406 std::fs::create_dir_all(&self.config.base_dir)
407 .map_err(|e| IoError::FileError(format!("Cannot create dir: {e}")))?;
408
409 let shard_count = self.config.shard_count;
410 let total = data.len();
411
412 let mut buckets: Vec<Vec<f64>> = vec![Vec::new(); shard_count];
414 for (i, &v) in data.iter().enumerate() {
415 let s = match self.config.strategy {
416 ShardingStrategy::RoundRobin => RoundRobinSharding::shard_for(i, shard_count),
417 ShardingStrategy::Hash => HashSharding::shard_for(i, shard_count),
418 ShardingStrategy::Range => RangeSharding::shard_for(i, shard_count, total),
419 };
420 buckets[s].push(v);
421 }
422
423 let written: Arc<Mutex<Vec<(usize, Result<PathBuf>)>>> =
424 Arc::new(Mutex::new(Vec::with_capacity(shard_count)));
425
426 let handles: Vec<_> = buckets
427 .into_iter()
428 .enumerate()
429 .map(|(idx, bucket)| {
430 let path = self.config.shard_path(idx);
431 let written = written.clone();
432 thread::spawn(move || {
433 let result = write_shard_raw(&path, &bucket).map(|()| path.clone());
434 let mut guard = written.lock().expect("lock poisoned");
435 guard.push((idx, result));
436 })
437 })
438 .collect();
439
440 for h in handles {
441 h.join()
442 .map_err(|_| IoError::FileError("Shard writer thread panicked".to_string()))?;
443 }
444
445 let mut guard = written.lock().expect("lock poisoned");
446 guard.sort_by_key(|(idx, _)| *idx);
447 let mut paths = Vec::with_capacity(shard_count);
448 for (_, result) in guard.drain(..) {
449 paths.push(result?);
450 }
451 Ok(paths)
452 }
453}
454
455fn write_shard_raw(path: &Path, data: &[f64]) -> Result<()> {
456 let mut f = std::fs::File::create(path)
457 .map_err(|e| IoError::FileError(format!("Cannot create shard: {e}")))?;
458 for &v in data {
459 f.write_all(&v.to_le_bytes())
460 .map_err(|e| IoError::FileError(format!("Shard write error: {e}")))?;
461 }
462 Ok(())
463}
464
465pub struct VirtualConcatenation {
473 paths: Vec<PathBuf>,
474 element_type: ElementType,
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum ElementType {
480 F64,
482 F32,
484 I64,
486}
487
488impl VirtualConcatenation {
489 pub fn new(paths: Vec<PathBuf>, element_type: ElementType) -> Self {
491 Self {
492 paths,
493 element_type,
494 }
495 }
496
497 pub fn collect_f64(&self) -> Result<Vec<f64>> {
501 let mut out = Vec::new();
502 for path in &self.paths {
503 let mut f = std::fs::File::open(path)
504 .map_err(|_| IoError::NotFound(format!("File not found: {}", path.display())))?;
505 let mut buf = Vec::new();
506 f.read_to_end(&mut buf)
507 .map_err(|e| IoError::FileError(format!("Read error: {e}")))?;
508 match self.element_type {
509 ElementType::F64 => {
510 for b in buf.chunks_exact(8) {
511 let arr: [u8; 8] = b.try_into().unwrap_or([0u8; 8]);
512 out.push(f64::from_le_bytes(arr));
513 }
514 }
515 ElementType::F32 => {
516 for b in buf.chunks_exact(4) {
517 let arr: [u8; 4] = b.try_into().unwrap_or([0u8; 4]);
518 out.push(f32::from_le_bytes(arr) as f64);
519 }
520 }
521 ElementType::I64 => {
522 for b in buf.chunks_exact(8) {
523 let arr: [u8; 8] = b.try_into().unwrap_or([0u8; 8]);
524 out.push(i64::from_le_bytes(arr) as f64);
525 }
526 }
527 }
528 }
529 Ok(out)
530 }
531
532 pub fn estimated_len(&self) -> usize {
534 let bytes_per = match self.element_type {
535 ElementType::F64 | ElementType::I64 => 8,
536 ElementType::F32 => 4,
537 };
538 self.paths
539 .iter()
540 .filter_map(|p| std::fs::metadata(p).ok())
541 .map(|m| m.len() as usize / bytes_per)
542 .sum()
543 }
544
545 pub fn shard_count(&self) -> usize {
547 self.paths.len()
548 }
549}
550
551#[cfg(test)]
556mod tests {
557 use super::*;
558 use std::env::temp_dir;
559 use uuid::Uuid;
560
561 fn temp_config(strategy: ShardingStrategy) -> ShardConfig {
562 let dir = temp_dir().join(format!("scirs2_shard_{}", Uuid::new_v4()));
563 ShardConfig::new(&dir, 4).with_strategy(strategy)
564 }
565
566 #[test]
567 fn test_range_sharding() {
568 let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
569 let config = temp_config(ShardingStrategy::Range);
570 let mut sa = ShardedArray::new(config.clone());
571 let meta = sa.write(&data).unwrap();
572 assert_eq!(meta.total_elements, 100);
573 assert_eq!(meta.shard_sizes.iter().sum::<usize>(), 100);
574 let loaded = sa.read().expect("range sharded read should succeed");
575 assert_eq!(loaded, data);
576 let _ = std::fs::remove_dir_all(&config.base_dir);
577 }
578
579 #[test]
580 fn test_round_robin_sharding() {
581 let data: Vec<f64> = (0..20).map(|i| i as f64).collect();
582 let config = temp_config(ShardingStrategy::RoundRobin);
583 let mut sa = ShardedArray::new(config.clone());
584 sa.write(&data).unwrap();
585 let loaded = sa.read().expect("round-robin sharded read should succeed");
586 assert_eq!(loaded, data);
587 let _ = std::fs::remove_dir_all(&config.base_dir);
588 }
589
590 #[test]
591 fn test_hash_sharding() {
592 let data: Vec<f64> = (0..20).map(|i| i as f64 * 2.0).collect();
593 let config = temp_config(ShardingStrategy::Hash);
594 let mut sa = ShardedArray::new(config.clone());
595 sa.write(&data).unwrap();
596 let loaded = sa.read().expect("hash sharded read should succeed");
597 assert_eq!(loaded, data);
598 let _ = std::fs::remove_dir_all(&config.base_dir);
599 }
600
601 #[test]
602 fn test_shard_writer_and_reader() {
603 let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
604 let config = temp_config(ShardingStrategy::Range);
605 let writer = ShardWriter::new(config.clone());
606 let paths = writer.write(&data).unwrap();
607 assert_eq!(paths.len(), 4);
608 let reader = ShardReader::new(config.clone());
609 let merged = reader.read_all().unwrap();
610 assert_eq!(merged, data);
611 let _ = std::fs::remove_dir_all(&config.base_dir);
612 }
613
614 #[test]
615 fn test_virtual_concatenation() {
616 let dir = temp_dir().join(format!("scirs2_vc_{}", Uuid::new_v4()));
617 std::fs::create_dir_all(&dir).unwrap();
618
619 let mut paths = Vec::new();
620 for i in 0..3usize {
621 let p = dir.join(format!("part_{i}.bin"));
622 let mut f = std::fs::File::create(&p).unwrap();
623 for v in (i * 10..(i + 1) * 10).map(|x| (x as f64).to_le_bytes()) {
624 f.write_all(&v).unwrap();
625 }
626 paths.push(p);
627 }
628
629 let vc = VirtualConcatenation::new(paths, ElementType::F64);
630 let data = vc.collect_f64().unwrap();
631 assert_eq!(data.len(), 30);
632 assert_eq!(data[0], 0.0);
633 assert_eq!(data[29], 29.0);
634 assert_eq!(vc.estimated_len(), 30);
635 let _ = std::fs::remove_dir_all(&dir);
636 }
637}