1use std::collections::HashMap;
7
8#[inline]
13fn fnv1a_64(data: &[u8]) -> u64 {
14 let mut hash: u64 = 0xcbf29ce484222325;
15 for &byte in data {
16 hash ^= byte as u64;
17 hash = hash.wrapping_mul(0x100000001b3);
18 }
19 hash
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub struct ShardKey(pub u8);
29
30impl ShardKey {
31 #[inline]
33 pub fn index(self) -> u8 {
34 self.0
35 }
36
37 #[inline]
39 pub fn as_usize(self) -> usize {
40 self.0 as usize
41 }
42}
43
44#[derive(Clone, Debug)]
50pub struct BlockRecord {
51 pub cid: String,
53 pub data: Vec<u8>,
55 pub shard: ShardKey,
57 pub inserted_at: u64,
59 pub access_count: u64,
61}
62
63#[derive(Clone, Debug)]
69pub struct ShardingConfig {
70 pub num_shards: u8,
72 pub max_blocks_per_shard: usize,
74 pub rebalance_threshold: f64,
76}
77
78impl Default for ShardingConfig {
79 fn default() -> Self {
80 Self {
81 num_shards: 16,
82 max_blocks_per_shard: 65536,
83 rebalance_threshold: 0.25,
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default)]
94pub struct ShardMetrics {
95 pub shard_id: u8,
97 pub block_count: usize,
99 pub total_bytes: u64,
101 pub hit_count: u64,
103 pub miss_count: u64,
105}
106
107#[derive(Clone, Debug)]
113pub struct BlockStoreGlobalStats {
114 pub total_blocks: usize,
116 pub total_bytes: u64,
118 pub total_insertions: u64,
120 pub total_lookups: u64,
122 pub num_shards: u8,
124 pub needs_rebalance: bool,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum ShardingError {
135 ShardFull {
137 shard_id: u8,
139 },
140 CidNotFound(String),
142 InvalidShardId(u8),
144}
145
146impl std::fmt::Display for ShardingError {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 Self::ShardFull { shard_id } => {
150 write!(f, "shard {} is full", shard_id)
151 }
152 Self::CidNotFound(cid) => write!(f, "CID not found: {}", cid),
153 Self::InvalidShardId(id) => write!(f, "invalid shard id: {}", id),
154 }
155 }
156}
157
158impl std::error::Error for ShardingError {}
159
160pub struct BlockStoreSharding {
169 pub config: ShardingConfig,
171 shards: Vec<HashMap<String, BlockRecord>>,
173 shard_metrics: Vec<ShardMetrics>,
175 pub total_insertions: u64,
177 pub total_lookups: u64,
179 pub total_bytes: u64,
181}
182
183impl BlockStoreSharding {
184 pub fn new(config: ShardingConfig) -> Self {
192 assert!(config.num_shards > 0, "num_shards must be > 0");
193 let n = config.num_shards as usize;
194 let mut shard_metrics: Vec<ShardMetrics> = Vec::with_capacity(n);
195 for i in 0..n {
196 shard_metrics.push(ShardMetrics {
197 shard_id: i as u8,
198 ..ShardMetrics::default()
199 });
200 }
201 Self {
202 shards: (0..n).map(|_| HashMap::new()).collect(),
203 shard_metrics,
204 config,
205 total_insertions: 0,
206 total_lookups: 0,
207 total_bytes: 0,
208 }
209 }
210
211 pub fn shard_for_cid(&self, cid: &str) -> ShardKey {
217 let hash = fnv1a_64(cid.as_bytes());
218 let idx = (hash % self.config.num_shards as u64) as u8;
219 ShardKey(idx)
220 }
221
222 pub fn put(&mut self, cid: String, data: Vec<u8>, now: u64) -> Result<bool, ShardingError> {
233 let key = self.shard_for_cid(&cid);
234 let idx = key.as_usize();
235
236 let shard = &mut self.shards[idx];
237 let metrics = &mut self.shard_metrics[idx];
238
239 if let Some(existing) = shard.get_mut(&cid) {
240 self.total_bytes = self
242 .total_bytes
243 .saturating_sub(existing.data.len() as u64)
244 .saturating_add(data.len() as u64);
245 metrics.total_bytes = metrics
246 .total_bytes
247 .saturating_sub(existing.data.len() as u64)
248 .saturating_add(data.len() as u64);
249 existing.data = data;
250 existing.inserted_at = now;
251 self.total_insertions = self.total_insertions.saturating_add(1);
252 return Ok(false);
253 }
254
255 if shard.len() >= self.config.max_blocks_per_shard {
257 return Err(ShardingError::ShardFull { shard_id: key.0 });
258 }
259
260 let byte_len = data.len() as u64;
261 let record = BlockRecord {
262 cid: cid.clone(),
263 data,
264 shard: key,
265 inserted_at: now,
266 access_count: 0,
267 };
268 shard.insert(cid, record);
269 metrics.block_count += 1;
270 metrics.total_bytes = metrics.total_bytes.saturating_add(byte_len);
271 self.total_bytes = self.total_bytes.saturating_add(byte_len);
272 self.total_insertions = self.total_insertions.saturating_add(1);
273 Ok(true)
274 }
275
276 pub fn get(&mut self, cid: &str) -> Option<&BlockRecord> {
285 let key = self.shard_for_cid(cid);
286 let idx = key.as_usize();
287 self.total_lookups = self.total_lookups.saturating_add(1);
288
289 if self.shards[idx].contains_key(cid) {
290 self.shard_metrics[idx].hit_count = self.shard_metrics[idx].hit_count.saturating_add(1);
291 let record = self.shards[idx].get_mut(cid)?;
292 record.access_count = record.access_count.saturating_add(1);
293 Some(record)
294 } else {
295 self.shard_metrics[idx].miss_count =
296 self.shard_metrics[idx].miss_count.saturating_add(1);
297 None
298 }
299 }
300
301 pub fn get_mut(&mut self, cid: &str) -> Option<&mut BlockRecord> {
303 let key = self.shard_for_cid(cid);
304 let idx = key.as_usize();
305 self.shards[idx].get_mut(cid)
306 }
307
308 pub fn contains(&self, cid: &str) -> bool {
310 let key = self.shard_for_cid(cid);
311 self.shards[key.as_usize()].contains_key(cid)
312 }
313
314 pub fn remove(&mut self, cid: &str) -> bool {
322 let key = self.shard_for_cid(cid);
323 let idx = key.as_usize();
324 if let Some(record) = self.shards[idx].remove(cid) {
325 let byte_len = record.data.len() as u64;
326 self.shard_metrics[idx].block_count =
327 self.shard_metrics[idx].block_count.saturating_sub(1);
328 self.shard_metrics[idx].total_bytes =
329 self.shard_metrics[idx].total_bytes.saturating_sub(byte_len);
330 self.total_bytes = self.total_bytes.saturating_sub(byte_len);
331 true
332 } else {
333 false
334 }
335 }
336
337 pub fn shard_metrics(&self, shard: ShardKey) -> Option<&ShardMetrics> {
344 self.shard_metrics.get(shard.as_usize())
345 }
346
347 pub fn all_shard_metrics(&self) -> &[ShardMetrics] {
349 &self.shard_metrics
350 }
351
352 pub fn needs_rebalance(&self) -> bool {
360 let total = self.total_block_count();
361 if total == 0 {
362 return false;
363 }
364 let avg = total as f64 / self.config.num_shards as f64;
365 let ceiling = avg * (1.0 + self.config.rebalance_threshold);
366 self.shards.iter().any(|s| s.len() as f64 > ceiling)
367 }
368
369 pub fn rebalance(&mut self, now: u64) -> usize {
381 let total = self.total_block_count();
382 if total == 0 {
383 return 0;
384 }
385 let avg = total as f64 / self.config.num_shards as f64;
386 let ceiling = avg * (1.0 + self.config.rebalance_threshold);
387
388 let overloaded: Vec<(usize, usize)> = self
390 .shards
391 .iter()
392 .enumerate()
393 .filter_map(|(i, s)| {
394 let len = s.len() as f64;
395 if len > ceiling {
396 let overflow = (len - avg.ceil()) as usize;
397 Some((i, overflow))
398 } else {
399 None
400 }
401 })
402 .collect();
403
404 if overloaded.is_empty() {
405 return 0;
406 }
407
408 let mut moved_total = 0usize;
409
410 for (shard_idx, overflow_count) in overloaded {
411 let mut cids_sorted: Vec<(String, u64)> = self.shards[shard_idx]
413 .values()
414 .map(|r| (r.cid.clone(), r.access_count))
415 .collect();
416 cids_sorted.sort_by_key(|(_, ac)| *ac);
417 cids_sorted.truncate(overflow_count);
418
419 let mut to_move: Vec<BlockRecord> = Vec::with_capacity(cids_sorted.len());
421 for (cid, _) in &cids_sorted {
422 if let Some(mut record) = self.shards[shard_idx].remove(cid.as_str()) {
423 let byte_len = record.data.len() as u64;
424 self.shard_metrics[shard_idx].block_count =
425 self.shard_metrics[shard_idx].block_count.saturating_sub(1);
426 self.shard_metrics[shard_idx].total_bytes = self.shard_metrics[shard_idx]
427 .total_bytes
428 .saturating_sub(byte_len);
429 self.total_bytes = self.total_bytes.saturating_sub(byte_len);
430 record.inserted_at = now;
431 to_move.push(record);
432 }
433 }
434
435 for mut record in to_move {
437 let natural_key = self.shard_for_cid(&record.cid);
438 let nat_idx = natural_key.as_usize();
439 record.shard = natural_key;
440 let byte_len = record.data.len() as u64;
441 self.shards[nat_idx].insert(record.cid.clone(), record);
442 self.shard_metrics[nat_idx].block_count += 1;
443 self.shard_metrics[nat_idx].total_bytes = self.shard_metrics[nat_idx]
444 .total_bytes
445 .saturating_add(byte_len);
446 self.total_bytes = self.total_bytes.saturating_add(byte_len);
447 moved_total += 1;
448 }
449 }
450
451 moved_total
452 }
453
454 pub fn evict_lru(&mut self, shard: ShardKey, count: usize) -> usize {
464 let idx = shard.as_usize();
465 if idx >= self.shards.len() || count == 0 {
466 return 0;
467 }
468
469 let mut cids_sorted: Vec<(String, u64)> = self.shards[idx]
470 .values()
471 .map(|r| (r.cid.clone(), r.access_count))
472 .collect();
473 cids_sorted.sort_by_key(|(_, ac)| *ac);
474 let to_evict = count.min(cids_sorted.len());
475
476 let mut evicted = 0usize;
477 for (cid, _) in cids_sorted.into_iter().take(to_evict) {
478 if let Some(record) = self.shards[idx].remove(&cid) {
479 let byte_len = record.data.len() as u64;
480 self.shard_metrics[idx].block_count =
481 self.shard_metrics[idx].block_count.saturating_sub(1);
482 self.shard_metrics[idx].total_bytes =
483 self.shard_metrics[idx].total_bytes.saturating_sub(byte_len);
484 self.total_bytes = self.total_bytes.saturating_sub(byte_len);
485 evicted += 1;
486 }
487 }
488 evicted
489 }
490
491 pub fn cids_in_shard(&self, shard: ShardKey) -> Vec<&str> {
497 let idx = shard.as_usize();
498 if idx >= self.shards.len() {
499 return Vec::new();
500 }
501 self.shards[idx].keys().map(|k| k.as_str()).collect()
502 }
503
504 pub fn total_block_count(&self) -> usize {
506 self.shards.iter().map(|s| s.len()).sum()
507 }
508
509 pub fn total_bytes(&self) -> u64 {
511 self.total_bytes
512 }
513
514 pub fn global_stats(&self) -> BlockStoreGlobalStats {
516 BlockStoreGlobalStats {
517 total_blocks: self.total_block_count(),
518 total_bytes: self.total_bytes,
519 total_insertions: self.total_insertions,
520 total_lookups: self.total_lookups,
521 num_shards: self.config.num_shards,
522 needs_rebalance: self.needs_rebalance(),
523 }
524 }
525}
526
527#[cfg(test)]
532mod tests {
533 use super::{
534 fnv1a_64, BlockRecord, BlockStoreGlobalStats, BlockStoreSharding, ShardKey, ShardMetrics,
535 ShardingConfig, ShardingError,
536 };
537
538 fn default_store() -> BlockStoreSharding {
543 BlockStoreSharding::new(ShardingConfig::default())
544 }
545
546 fn small_store(num_shards: u8) -> BlockStoreSharding {
547 BlockStoreSharding::new(ShardingConfig {
548 num_shards,
549 max_blocks_per_shard: 100,
550 rebalance_threshold: 0.25,
551 })
552 }
553
554 fn make_cid(n: usize) -> String {
555 format!("bafy2bzaced{:040}", n)
556 }
557
558 #[allow(dead_code)]
559 fn insert(store: &mut BlockStoreSharding, cid: &str, data: &[u8]) -> bool {
560 store
561 .put(cid.to_string(), data.to_vec(), 1000)
562 .expect("insert failed")
563 }
564
565 #[test]
569 fn test_new_default_shards() {
570 let store = default_store();
571 assert_eq!(store.config.num_shards, 16);
572 assert_eq!(store.shards.len(), 16);
573 assert_eq!(store.shard_metrics.len(), 16);
574 }
575
576 #[test]
578 fn test_new_custom_shards() {
579 let store = small_store(4);
580 assert_eq!(store.config.num_shards, 4);
581 assert_eq!(store.shards.len(), 4);
582 }
583
584 #[test]
586 fn test_shard_for_cid_in_range() {
587 let store = default_store();
588 for i in 0..100usize {
589 let cid = make_cid(i);
590 let key = store.shard_for_cid(&cid);
591 assert!(key.0 < store.config.num_shards);
592 }
593 }
594
595 #[test]
597 fn test_shard_for_cid_deterministic() {
598 let store = default_store();
599 let cid = make_cid(42);
600 assert_eq!(store.shard_for_cid(&cid), store.shard_for_cid(&cid));
601 }
602
603 #[test]
605 fn test_shard_for_cid_formula() {
606 let store = small_store(8);
607 let cid = "QmTestCid12345";
608 let expected_idx = (fnv1a_64(cid.as_bytes()) % 8) as u8;
609 assert_eq!(store.shard_for_cid(cid).0, expected_idx);
610 }
611
612 #[test]
614 fn test_put_new_returns_true() {
615 let mut store = default_store();
616 let result = store.put("cid1".to_string(), vec![1, 2, 3], 100);
617 assert_eq!(result, Ok(true));
618 }
619
620 #[test]
622 fn test_put_replace_returns_false() {
623 let mut store = default_store();
624 store
625 .put("cid1".to_string(), vec![1, 2, 3], 100)
626 .expect("first insert");
627 let result = store.put("cid1".to_string(), vec![4, 5, 6], 200);
628 assert_eq!(result, Ok(false));
629 }
630
631 #[test]
633 fn test_put_shard_full() {
634 let mut store = BlockStoreSharding::new(ShardingConfig {
635 num_shards: 1,
636 max_blocks_per_shard: 2,
637 rebalance_threshold: 0.25,
638 });
639 store.put("a".to_string(), vec![1], 0).expect("a");
640 store.put("b".to_string(), vec![2], 0).expect("b");
641 let result = store.put("c".to_string(), vec![3], 0);
642 assert!(matches!(
643 result,
644 Err(ShardingError::ShardFull { shard_id: 0 })
645 ));
646 }
647
648 #[test]
650 fn test_put_replace_no_capacity_loss() {
651 let mut store = BlockStoreSharding::new(ShardingConfig {
652 num_shards: 1,
653 max_blocks_per_shard: 1,
654 rebalance_threshold: 0.25,
655 });
656 store.put("a".to_string(), vec![1], 0).expect("first");
657 let result = store.put("a".to_string(), vec![99], 1);
659 assert_eq!(result, Ok(false));
660 }
661
662 #[test]
664 fn test_total_insertions() {
665 let mut store = default_store();
666 store.put("c1".to_string(), vec![1], 0).expect("c1");
667 store.put("c2".to_string(), vec![2], 0).expect("c2");
668 store.put("c1".to_string(), vec![3], 1).expect("replace c1");
669 assert_eq!(store.total_insertions, 3);
670 }
671
672 #[test]
674 fn test_get_returns_record() {
675 let mut store = default_store();
676 store
677 .put("cid_x".to_string(), vec![10, 20], 500)
678 .expect("put");
679 let record = store.get("cid_x").expect("should exist");
680 assert_eq!(record.cid, "cid_x");
681 assert_eq!(record.data, vec![10, 20]);
682 assert_eq!(record.inserted_at, 500);
683 }
684
685 #[test]
687 fn test_get_increments_access_count() {
688 let mut store = default_store();
689 store.put("cid_ac".to_string(), vec![1], 0).expect("put");
690 store.get("cid_ac");
691 store.get("cid_ac");
692 let record = store.get("cid_ac").expect("record");
693 assert_eq!(record.access_count, 3);
694 }
695
696 #[test]
698 fn test_get_updates_hit_count() {
699 let mut store = default_store();
700 store.put("cid_h".to_string(), vec![1], 0).expect("put");
701 let key = store.shard_for_cid("cid_h");
702 store.get("cid_h");
703 store.get("cid_h");
704 assert_eq!(store.shard_metrics(key).expect("metrics").hit_count, 2);
705 }
706
707 #[test]
709 fn test_get_miss_updates_miss_count() {
710 let mut store = default_store();
711 let key = store.shard_for_cid("nonexistent");
712 store.get("nonexistent");
713 assert_eq!(store.shard_metrics(key).expect("metrics").miss_count, 1);
714 assert!(store.get("nonexistent").is_none());
715 }
716
717 #[test]
719 fn test_get_increments_total_lookups() {
720 let mut store = default_store();
721 store.put("lk".to_string(), vec![1], 0).expect("put");
722 store.get("lk");
723 store.get("missing");
724 assert_eq!(store.total_lookups, 2);
725 }
726
727 #[test]
729 fn test_get_mut_returns_mutable() {
730 let mut store = default_store();
731 store.put("m".to_string(), vec![5], 0).expect("put");
732 {
733 let record = store.get_mut("m").expect("should exist");
734 record.access_count = 99;
735 }
736 let record = store.get_mut("m").expect("still exists");
737 assert_eq!(record.access_count, 99);
738 }
739
740 #[test]
742 fn test_contains_true() {
743 let mut store = default_store();
744 store.put("x".to_string(), vec![1], 0).expect("put");
745 assert!(store.contains("x"));
746 }
747
748 #[test]
750 fn test_contains_false() {
751 let store = default_store();
752 assert!(!store.contains("zzz"));
753 }
754
755 #[test]
757 fn test_remove_existing() {
758 let mut store = default_store();
759 store.put("r".to_string(), vec![1, 2, 3], 0).expect("put");
760 assert!(store.remove("r"));
761 assert!(!store.contains("r"));
762 }
763
764 #[test]
766 fn test_remove_updates_total_bytes() {
767 let mut store = default_store();
768 store
769 .put("rb".to_string(), vec![1, 2, 3, 4], 0)
770 .expect("put");
771 let before = store.total_bytes();
772 store.remove("rb");
773 assert_eq!(store.total_bytes(), before - 4);
774 }
775
776 #[test]
778 fn test_remove_missing_returns_false() {
779 let mut store = default_store();
780 assert!(!store.remove("no_such"));
781 }
782
783 #[test]
785 fn test_total_block_count() {
786 let mut store = default_store();
787 for i in 0..20usize {
788 store.put(make_cid(i), vec![i as u8], 0).expect("put");
789 }
790 assert_eq!(store.total_block_count(), 20);
791 }
792
793 #[test]
795 fn test_total_bytes_consistency() {
796 let mut store = default_store();
797 store
798 .put("a".to_string(), vec![0u8; 100], 0)
799 .expect("put a");
800 store
801 .put("b".to_string(), vec![0u8; 200], 0)
802 .expect("put b");
803 assert_eq!(store.total_bytes(), 300);
804 store.remove("a");
805 assert_eq!(store.total_bytes(), 200);
806 }
807
808 #[test]
810 fn test_shard_metrics_correct_id() {
811 let store = small_store(4);
812 for i in 0..4u8 {
813 let m = store.shard_metrics(ShardKey(i)).expect("metrics");
814 assert_eq!(m.shard_id, i);
815 }
816 }
817
818 #[test]
820 fn test_shard_metrics_out_of_range() {
821 let store = small_store(4);
822 assert!(store.shard_metrics(ShardKey(4)).is_none());
823 }
824
825 #[test]
827 fn test_all_shard_metrics_length() {
828 let store = small_store(8);
829 assert_eq!(store.all_shard_metrics().len(), 8);
830 }
831
832 #[test]
834 fn test_cids_in_shard() {
835 let mut store = small_store(1);
836 store.put("p".to_string(), vec![1], 0).expect("put");
837 store.put("q".to_string(), vec![2], 0).expect("put");
838 let mut cids = store.cids_in_shard(ShardKey(0));
839 cids.sort_unstable();
840 assert!(cids.contains(&"p"));
841 assert!(cids.contains(&"q"));
842 }
843
844 #[test]
846 fn test_cids_in_shard_out_of_range() {
847 let store = small_store(2);
848 assert!(store.cids_in_shard(ShardKey(99)).is_empty());
849 }
850
851 #[test]
853 fn test_needs_rebalance_empty() {
854 let store = default_store();
855 assert!(!store.needs_rebalance());
856 }
857
858 #[test]
860 fn test_needs_rebalance_even() {
861 let mut store = BlockStoreSharding::new(ShardingConfig {
864 num_shards: 1,
865 max_blocks_per_shard: 10000,
866 rebalance_threshold: 0.25,
867 });
868 for i in 0..5usize {
869 store.put(make_cid(i), vec![1], 0).expect("put");
870 }
871 assert!(!store.needs_rebalance());
873 }
874
875 #[test]
877 fn test_needs_rebalance_imbalanced() {
878 let mut store = BlockStoreSharding::new(ShardingConfig {
881 num_shards: 2,
882 max_blocks_per_shard: 10000,
883 rebalance_threshold: 0.10,
884 });
885 let mut shard0_count = 0usize;
890 let mut i = 0usize;
891 while shard0_count < 20 {
892 let cid = format!("rebal_cid_{}", i);
893 let key = store.shard_for_cid(&cid);
894 if key.0 == 0 {
895 store.put(cid, vec![1], 0).expect("put");
896 shard0_count += 1;
897 }
898 i += 1;
899 }
900 assert!(store.needs_rebalance());
902 }
903
904 #[test]
906 fn test_rebalance_empty() {
907 let mut store = default_store();
908 assert_eq!(store.rebalance(0), 0);
909 }
910
911 #[test]
913 fn test_rebalance_reduces_imbalance() {
914 let mut store = BlockStoreSharding::new(ShardingConfig {
915 num_shards: 2,
916 max_blocks_per_shard: 10000,
917 rebalance_threshold: 0.10,
918 });
919 let mut shard0_count = 0usize;
920 let mut i = 0usize;
921 while shard0_count < 20 {
922 let cid = format!("rb_cid_{}", i);
923 let key = store.shard_for_cid(&cid);
924 if key.0 == 0 {
925 store.put(cid, vec![1], 0).expect("put");
926 shard0_count += 1;
927 }
928 i += 1;
929 }
930 let total_before = store.total_block_count();
931 let _moved = store.rebalance(1000);
932 let total_after = store.total_block_count();
933 assert_eq!(total_before, total_after);
935 }
936
937 #[test]
939 fn test_evict_lru_count() {
940 let mut store = small_store(1);
941 for i in 0..10usize {
942 store.put(make_cid(i), vec![i as u8], 0).expect("put");
943 }
944 let evicted = store.evict_lru(ShardKey(0), 3);
945 assert_eq!(evicted, 3);
946 assert_eq!(store.total_block_count(), 7);
947 }
948
949 #[test]
951 fn test_evict_lru_lowest_access() {
952 let mut store = small_store(1);
953 store.put("hot".to_string(), vec![1], 0).expect("put");
954 store.put("cold".to_string(), vec![2], 0).expect("put");
955 for _ in 0..10 {
957 store.get("hot");
958 }
959 store.evict_lru(ShardKey(0), 1);
961 assert!(store.contains("hot"));
962 assert!(!store.contains("cold"));
963 }
964
965 #[test]
967 fn test_evict_lru_clamps_to_shard_size() {
968 let mut store = small_store(1);
969 for i in 0..5usize {
970 store.put(make_cid(i), vec![1], 0).expect("put");
971 }
972 let evicted = store.evict_lru(ShardKey(0), 100);
973 assert_eq!(evicted, 5);
974 assert_eq!(store.total_block_count(), 0);
975 }
976
977 #[test]
979 fn test_evict_lru_out_of_range() {
980 let mut store = small_store(2);
981 assert_eq!(store.evict_lru(ShardKey(99), 5), 0);
982 }
983
984 #[test]
986 fn test_global_stats_basic() {
987 let mut store = default_store();
988 store.put("g1".to_string(), vec![1, 2], 0).expect("put");
989 store.put("g2".to_string(), vec![3, 4, 5], 0).expect("put");
990 let stats = store.global_stats();
991 assert_eq!(stats.total_blocks, 2);
992 assert_eq!(stats.total_bytes, 5);
993 assert_eq!(stats.total_insertions, 2);
994 assert_eq!(stats.num_shards, 16);
995 }
996
997 #[test]
999 fn test_global_stats_needs_rebalance_flag() {
1000 let store = default_store();
1001 let stats = store.global_stats();
1002 assert_eq!(stats.needs_rebalance, store.needs_rebalance());
1003 }
1004
1005 #[test]
1007 fn test_replace_updates_bytes() {
1008 let mut store = default_store();
1009 store
1010 .put("replace_me".to_string(), vec![0u8; 10], 0)
1011 .expect("first");
1012 store
1013 .put("replace_me".to_string(), vec![0u8; 25], 1)
1014 .expect("replace");
1015 assert_eq!(store.total_bytes(), 25);
1016 }
1017
1018 #[test]
1020 fn test_shard_metrics_block_count() {
1021 let mut store = small_store(1);
1022 store.put("a".to_string(), vec![1], 0).expect("put");
1023 store.put("b".to_string(), vec![2], 0).expect("put");
1024 let m = store.shard_metrics(ShardKey(0)).expect("metrics");
1025 assert_eq!(m.block_count, 2);
1026 store.remove("a");
1027 let m2 = store.shard_metrics(ShardKey(0)).expect("metrics");
1028 assert_eq!(m2.block_count, 1);
1029 }
1030
1031 #[test]
1033 fn test_shard_metrics_total_bytes() {
1034 let mut store = small_store(1);
1035 store.put("x".to_string(), vec![0u8; 50], 0).expect("put");
1036 let m = store.shard_metrics(ShardKey(0)).expect("metrics");
1037 assert_eq!(m.total_bytes, 50);
1038 }
1039
1040 #[test]
1042 fn test_shard_key_accessors() {
1043 let k = ShardKey(7);
1044 assert_eq!(k.index(), 7);
1045 assert_eq!(k.as_usize(), 7);
1046 }
1047
1048 #[test]
1050 fn test_sharding_error_shard_full_id() {
1051 let mut store = BlockStoreSharding::new(ShardingConfig {
1052 num_shards: 1,
1053 max_blocks_per_shard: 0,
1054 rebalance_threshold: 0.25,
1055 });
1056 let result = store.put("any".to_string(), vec![1], 0);
1057 assert_eq!(result, Err(ShardingError::ShardFull { shard_id: 0 }));
1058 }
1059
1060 #[test]
1062 fn test_sharding_error_display() {
1063 let e1 = ShardingError::ShardFull { shard_id: 3 };
1064 let e2 = ShardingError::CidNotFound("abc".to_string());
1065 let e3 = ShardingError::InvalidShardId(99);
1066 assert!(e1.to_string().contains("3"));
1067 assert!(e2.to_string().contains("abc"));
1068 assert!(e3.to_string().contains("99"));
1069 }
1070
1071 #[test]
1073 fn test_block_record_fields() {
1074 let r = BlockRecord {
1075 cid: "test_cid".to_string(),
1076 data: vec![1, 2, 3],
1077 shard: ShardKey(2),
1078 inserted_at: 12345,
1079 access_count: 7,
1080 };
1081 assert_eq!(r.cid, "test_cid");
1082 assert_eq!(r.shard.0, 2);
1083 assert_eq!(r.inserted_at, 12345);
1084 assert_eq!(r.access_count, 7);
1085 }
1086
1087 #[test]
1089 fn test_global_stats_clone() {
1090 let s = BlockStoreGlobalStats {
1091 total_blocks: 10,
1092 total_bytes: 500,
1093 total_insertions: 12,
1094 total_lookups: 8,
1095 num_shards: 4,
1096 needs_rebalance: false,
1097 };
1098 let s2 = s.clone();
1099 assert_eq!(s2.total_blocks, 10);
1100 }
1101
1102 #[test]
1104 fn test_large_insertion_distribution() {
1105 let mut store = default_store();
1106 for i in 0..1000usize {
1107 store.put(make_cid(i), vec![1], 0).expect("put");
1108 }
1109 assert_eq!(store.total_block_count(), 1000);
1110 let non_empty = store
1112 .all_shard_metrics()
1113 .iter()
1114 .filter(|m| m.block_count > 0)
1115 .count();
1116 assert!(
1117 non_empty > 1,
1118 "blocks should be distributed across multiple shards"
1119 );
1120 }
1121
1122 #[test]
1124 fn test_shard_metrics_bytes_sum_equals_global() {
1125 let mut store = default_store();
1126 for i in 0..50usize {
1127 store.put(make_cid(i), vec![0u8; i + 1], 0).expect("put");
1128 }
1129 let metrics_sum: u64 = store
1130 .all_shard_metrics()
1131 .iter()
1132 .map(|m| m.total_bytes)
1133 .sum();
1134 assert_eq!(metrics_sum, store.total_bytes());
1135 }
1136
1137 #[test]
1139 fn test_shard_metrics_block_count_sum_equals_total() {
1140 let mut store = default_store();
1141 for i in 0..50usize {
1142 store.put(make_cid(i), vec![1], 0).expect("put");
1143 }
1144 let metrics_sum: usize = store
1145 .all_shard_metrics()
1146 .iter()
1147 .map(|m| m.block_count)
1148 .sum();
1149 assert_eq!(metrics_sum, store.total_block_count());
1150 }
1151
1152 #[test]
1154 fn test_shard_metrics_clone() {
1155 let m = ShardMetrics {
1156 shard_id: 3,
1157 block_count: 10,
1158 total_bytes: 500,
1159 hit_count: 7,
1160 miss_count: 2,
1161 };
1162 let m2 = m.clone();
1163 assert_eq!(m2.shard_id, 3);
1164 assert_eq!(m2.hit_count, 7);
1165 }
1166
1167 #[test]
1169 fn test_get_mut_no_metrics_update() {
1170 let mut store = small_store(1);
1171 store.put("z".to_string(), vec![1], 0).expect("put");
1172 store.get_mut("z");
1173 let m = store.shard_metrics(ShardKey(0)).expect("metrics");
1174 assert_eq!(m.hit_count, 0);
1175 assert_eq!(m.miss_count, 0);
1176 }
1177
1178 #[test]
1180 fn test_rebalance_preserves_bytes() {
1181 let mut store = BlockStoreSharding::new(ShardingConfig {
1182 num_shards: 2,
1183 max_blocks_per_shard: 10000,
1184 rebalance_threshold: 0.10,
1185 });
1186 let mut shard0_count = 0usize;
1187 let mut i = 0usize;
1188 while shard0_count < 10 {
1189 let cid = format!("rb_bytes_{}", i);
1190 let key = store.shard_for_cid(&cid);
1191 if key.0 == 0 {
1192 store.put(cid, vec![0u8; 8], 0).expect("put");
1193 shard0_count += 1;
1194 }
1195 i += 1;
1196 }
1197 let bytes_before = store.total_bytes();
1198 store.rebalance(1000);
1199 assert_eq!(store.total_bytes(), bytes_before);
1200 }
1201}