1use std::cell::Cell;
41use std::collections::HashMap;
42
43#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct NodeId(pub [u8; 32]);
50
51impl std::fmt::Debug for NodeId {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "NodeId(")?;
54 for b in &self.0 {
55 write!(f, "{b:02x}")?;
56 }
57 write!(f, ")")
58 }
59}
60
61impl std::fmt::Display for NodeId {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 for b in &self.0 {
64 write!(f, "{b:02x}")?;
65 }
66 Ok(())
67 }
68}
69
70impl NodeId {
71 #[inline]
73 pub fn xor_distance(&self, other: &NodeId) -> [u8; 32] {
74 let mut out = [0u8; 32];
75 for (o, (&a, &b)) in out.iter_mut().zip(self.0.iter().zip(other.0.iter())) {
76 *o = a ^ b;
77 }
78 out
79 }
80
81 #[inline]
85 pub fn leading_zeros(&self) -> u32 {
86 let mut count = 0u32;
87 for &byte in &self.0 {
88 if byte == 0 {
89 count += 8;
90 } else {
91 count += byte.leading_zeros();
92 break;
93 }
94 }
95 count
96 }
97
98 pub fn from_str_hex(s: &str) -> Option<NodeId> {
102 let s = s.trim();
103 if s.len() != 64 {
104 return None;
105 }
106 let mut bytes = [0u8; 32];
107 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
108 let hi = hex_nibble(chunk[0])?;
109 let lo = hex_nibble(chunk[1])?;
110 bytes[i] = (hi << 4) | lo;
111 }
112 Some(NodeId(bytes))
113 }
114}
115
116#[inline]
118fn hex_nibble(b: u8) -> Option<u8> {
119 match b {
120 b'0'..=b'9' => Some(b - b'0'),
121 b'a'..=b'f' => Some(b - b'a' + 10),
122 b'A'..=b'F' => Some(b - b'A' + 10),
123 _ => None,
124 }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
133pub struct ShardId(pub u8);
134
135#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct RoutingEntry {
142 pub node_id: NodeId,
144 pub addr: String,
146 pub last_seen: u64,
148 pub rtt_ms: u32,
150 pub shard: ShardId,
152}
153
154#[derive(Clone, Debug, Default, PartialEq, Eq)]
160pub enum EvictionPolicy {
161 #[default]
163 LeastRecentlySeen,
164 HighestRtt,
166 Random {
170 seed: u64,
172 },
173}
174
175#[derive(Clone, Debug)]
181pub struct ShardConfig {
182 pub num_shards: u8,
184 pub max_entries_per_shard: usize,
186 pub eviction_policy: EvictionPolicy,
188}
189
190impl Default for ShardConfig {
191 fn default() -> Self {
192 Self {
193 num_shards: 16,
194 max_entries_per_shard: 256,
195 eviction_policy: EvictionPolicy::LeastRecentlySeen,
196 }
197 }
198}
199
200#[derive(Clone, Debug, PartialEq)]
206pub struct ShardStats {
207 pub shard_id: u8,
209 pub entry_count: usize,
211 pub avg_rtt_ms: f64,
213 pub oldest_entry_ms: u64,
215}
216
217pub struct RoutingTableSharding {
227 pub config: ShardConfig,
229 pub shards: Vec<HashMap<NodeId, RoutingEntry>>,
231 pub total_insertions: u64,
233 pub total_evictions: u64,
235 pub total_lookups: Cell<u64>,
241 rng_state: u64,
243}
244
245impl RoutingTableSharding {
246 pub fn new(config: ShardConfig) -> Self {
248 let n = config.num_shards as usize;
249 let rng_state = match &config.eviction_policy {
252 EvictionPolicy::Random { seed } => {
253 if *seed == 0 {
254 1
255 } else {
256 *seed
257 }
258 }
259 _ => 6_364_136_223_846_793_005_u64, };
261 Self {
262 shards: (0..n).map(|_| HashMap::new()).collect(),
263 config,
264 total_insertions: 0,
265 total_evictions: 0,
266 total_lookups: Cell::new(0),
267 rng_state,
268 }
269 }
270
271 #[inline]
275 pub fn shard_for(&self, node_id: &NodeId) -> ShardId {
276 ShardId(node_id.0[0] % self.config.num_shards)
277 }
278
279 pub fn insert(&mut self, entry: RoutingEntry, _now: u64) -> bool {
288 let shard_idx = self.shard_for(&entry.node_id).0 as usize;
289 let shard = match self.shards.get_mut(shard_idx) {
290 Some(s) => s,
291 None => return false,
292 };
293 shard.insert(entry.node_id, entry);
294 self.total_insertions += 1;
295
296 if shard.len() > self.config.max_entries_per_shard {
297 self.evict_one(shard_idx);
298 true
299 } else {
300 false
301 }
302 }
303
304 pub fn remove(&mut self, node_id: &NodeId) -> bool {
306 let shard_idx = self.shard_for(node_id).0 as usize;
307 match self.shards.get_mut(shard_idx) {
308 Some(shard) => shard.remove(node_id).is_some(),
309 None => false,
310 }
311 }
312
313 pub fn get(&self, node_id: &NodeId) -> Option<&RoutingEntry> {
315 self.total_lookups_inc();
316 let shard_idx = self.shard_for(node_id).0 as usize;
317 self.shards.get(shard_idx)?.get(node_id)
318 }
319
320 pub fn update_rtt(&mut self, node_id: &NodeId, rtt_ms: u32, now: u64) -> bool {
324 let shard_idx = self.shard_for(node_id).0 as usize;
325 match self.shards.get_mut(shard_idx) {
326 Some(shard) => match shard.get_mut(node_id) {
327 Some(e) => {
328 e.rtt_ms = rtt_ms;
329 e.last_seen = now;
330 true
331 }
332 None => false,
333 },
334 None => false,
335 }
336 }
337
338 pub fn closest_nodes(&self, target: &NodeId, k: usize) -> Vec<&RoutingEntry> {
345 self.total_lookups_inc();
346 let mut all: Vec<(&NodeId, &RoutingEntry)> =
347 self.shards.iter().flat_map(|shard| shard.iter()).collect();
348
349 all.sort_by(|(a_id, _), (b_id, _)| {
350 let da = a_id.xor_distance(target);
351 let db = b_id.xor_distance(target);
352 da.cmp(&db)
353 });
354
355 all.into_iter().take(k).map(|(_, e)| e).collect()
356 }
357
358 pub fn nodes_in_shard(&self, shard: ShardId) -> Vec<&RoutingEntry> {
360 match self.shards.get(shard.0 as usize) {
361 Some(s) => s.values().collect(),
362 None => vec![],
363 }
364 }
365
366 pub fn evict_stale(&mut self, now: u64, max_age_ms: u64) -> usize {
370 let mut removed = 0usize;
371 for shard in &mut self.shards {
372 let before = shard.len();
373 shard.retain(|_, e| now.saturating_sub(e.last_seen) <= max_age_ms);
374 removed += before - shard.len();
375 }
376 removed
377 }
378
379 pub fn shard_stats(&self, shard: ShardId, now: u64) -> ShardStats {
383 let shard_idx = shard.0 as usize;
384 let shard_map = match self.shards.get(shard_idx) {
385 Some(s) => s,
386 None => {
387 return ShardStats {
388 shard_id: shard.0,
389 entry_count: 0,
390 avg_rtt_ms: 0.0,
391 oldest_entry_ms: 0,
392 };
393 }
394 };
395
396 let _ = now; let entry_count = shard_map.len();
398 if entry_count == 0 {
399 return ShardStats {
400 shard_id: shard.0,
401 entry_count: 0,
402 avg_rtt_ms: 0.0,
403 oldest_entry_ms: 0,
404 };
405 }
406
407 let sum_rtt: u64 = shard_map.values().map(|e| e.rtt_ms as u64).sum();
408 let avg_rtt_ms = sum_rtt as f64 / entry_count as f64;
409 let oldest_entry_ms = shard_map.values().map(|e| e.last_seen).min().unwrap_or(0);
410
411 ShardStats {
412 shard_id: shard.0,
413 entry_count,
414 avg_rtt_ms,
415 oldest_entry_ms,
416 }
417 }
418
419 pub fn all_stats(&self, now: u64) -> Vec<ShardStats> {
421 (0..self.config.num_shards)
422 .map(|i| self.shard_stats(ShardId(i), now))
423 .collect()
424 }
425
426 pub fn total_entries(&self) -> usize {
428 self.shards.iter().map(|s| s.len()).sum()
429 }
430
431 pub fn global_stats(&self) -> (u64, u64, u64) {
433 (
434 self.total_insertions,
435 self.total_evictions,
436 self.total_lookups.get(),
437 )
438 }
439
440 #[inline]
444 fn total_lookups_inc(&self) {
445 self.total_lookups
446 .set(self.total_lookups.get().wrapping_add(1));
447 }
448
449 fn evict_one(&mut self, shard_idx: usize) {
452 use EvictionPolicy::{HighestRtt, LeastRecentlySeen, Random};
453
454 let victim_id: Option<NodeId> = {
457 let shard = match self.shards.get(shard_idx) {
458 Some(s) => s,
459 None => return,
460 };
461
462 match &self.config.eviction_policy {
463 LeastRecentlySeen => shard
464 .values()
465 .min_by_key(|e| e.last_seen)
466 .map(|e| e.node_id),
467
468 HighestRtt => shard.values().max_by_key(|e| e.rtt_ms).map(|e| e.node_id),
469
470 Random { .. } => {
471 let keys: Vec<NodeId> = shard.keys().copied().collect();
473 if keys.is_empty() {
474 None
475 } else {
476 let mut state = self.rng_state;
478 state ^= state << 13;
479 state ^= state >> 7;
480 state ^= state << 17;
481 Some(keys[state as usize % keys.len()])
483 }
485 }
486 }
487 };
488
489 if matches!(&self.config.eviction_policy, EvictionPolicy::Random { .. }) {
491 let mut state = self.rng_state;
492 state ^= state << 13;
493 state ^= state >> 7;
494 state ^= state << 17;
495 self.rng_state = state;
496 }
497
498 if let Some(id) = victim_id {
499 if let Some(shard) = self.shards.get_mut(shard_idx) {
500 if shard.remove(&id).is_some() {
501 self.total_evictions += 1;
502 }
503 }
504 }
505 }
506}
507
508#[cfg(test)]
513mod tests {
514 use super::{EvictionPolicy, NodeId, RoutingEntry, RoutingTableSharding, ShardConfig, ShardId};
515
516 fn make_id(first_byte: u8) -> NodeId {
519 let mut b = [0u8; 32];
520 b[0] = first_byte;
521 NodeId(b)
522 }
523
524 fn make_id_full(bytes: [u8; 32]) -> NodeId {
525 NodeId(bytes)
526 }
527
528 fn make_entry(
529 id: NodeId,
530 addr: &str,
531 last_seen: u64,
532 rtt_ms: u32,
533 shard: ShardId,
534 ) -> RoutingEntry {
535 RoutingEntry {
536 node_id: id,
537 addr: addr.to_string(),
538 last_seen,
539 rtt_ms,
540 shard,
541 }
542 }
543
544 fn default_table() -> RoutingTableSharding {
545 RoutingTableSharding::new(ShardConfig::default())
546 }
547
548 #[test]
553 fn node_id_xor_distance_identity() {
554 let id = make_id(0xAB);
555 let dist = id.xor_distance(&id);
556 assert_eq!(dist, [0u8; 32]);
557 }
558
559 #[test]
560 fn node_id_xor_distance_known_value() {
561 let a = make_id(0b0000_0001);
562 let b = make_id(0b0000_0011);
563 let dist = a.xor_distance(&b);
564 assert_eq!(dist[0], 0b0000_0010);
565 assert_eq!(&dist[1..], &[0u8; 31]);
566 }
567
568 #[test]
569 fn node_id_leading_zeros_all_zero() {
570 let id = NodeId([0u8; 32]);
571 assert_eq!(id.leading_zeros(), 256);
572 }
573
574 #[test]
575 fn node_id_leading_zeros_first_bit_set() {
576 let id = make_id(0x80); assert_eq!(id.leading_zeros(), 0);
578 }
579
580 #[test]
581 fn node_id_leading_zeros_second_bit_set() {
582 let id = make_id(0x40); assert_eq!(id.leading_zeros(), 1);
584 }
585
586 #[test]
587 fn node_id_leading_zeros_cross_byte_boundary() {
588 let mut b = [0u8; 32];
589 b[1] = 0x80; let id = NodeId(b);
591 assert_eq!(id.leading_zeros(), 8);
592 }
593
594 #[test]
595 fn node_id_from_str_hex_valid() {
596 let hex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
597 let id = NodeId::from_str_hex(hex).expect("valid hex");
598 assert_eq!(id.0[0], 0x01);
599 assert_eq!(id.0[31], 0x20);
600 }
601
602 #[test]
603 fn node_id_from_str_hex_invalid_length() {
604 assert!(NodeId::from_str_hex("0102").is_none());
605 }
606
607 #[test]
608 fn node_id_from_str_hex_invalid_char() {
609 let bad = "zz02030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
610 assert!(NodeId::from_str_hex(bad).is_none());
611 }
612
613 #[test]
614 fn node_id_from_str_hex_roundtrip() {
615 let id = make_id(0xDE);
616 let hex = format!("{id}");
617 let id2 = NodeId::from_str_hex(&hex).expect("roundtrip");
618 assert_eq!(id, id2);
619 }
620
621 #[test]
622 fn node_id_ordering() {
623 let a = make_id(1);
624 let b = make_id(2);
625 assert!(a < b);
626 }
627
628 #[test]
633 fn shard_config_default_values() {
634 let cfg = ShardConfig::default();
635 assert_eq!(cfg.num_shards, 16);
636 assert_eq!(cfg.max_entries_per_shard, 256);
637 assert_eq!(cfg.eviction_policy, EvictionPolicy::LeastRecentlySeen);
638 }
639
640 #[test]
645 fn shard_for_is_first_byte_mod_num_shards() {
646 let table = default_table();
647 for first in 0u8..=255 {
648 let id = make_id(first);
649 assert_eq!(table.shard_for(&id).0, first % 16);
650 }
651 }
652
653 #[test]
654 fn shard_for_single_shard_always_zero() {
655 let cfg = ShardConfig {
656 num_shards: 1,
657 ..Default::default()
658 };
659 let table = RoutingTableSharding::new(cfg);
660 for first in 0u8..=255 {
661 assert_eq!(table.shard_for(&make_id(first)).0, 0);
662 }
663 }
664
665 #[test]
670 fn insert_and_get_basic() {
671 let mut table = default_table();
672 let id = make_id(0x00);
673 let shard = table.shard_for(&id);
674 let entry = make_entry(id, "127.0.0.1:4001", 1000, 5, shard);
675 let evicted = table.insert(entry, 1000);
676 assert!(!evicted);
677 let got = table.get(&id).expect("entry present");
678 assert_eq!(got.addr, "127.0.0.1:4001");
679 assert_eq!(got.rtt_ms, 5);
680 }
681
682 #[test]
683 fn insert_increments_total_insertions() {
684 let mut table = default_table();
685 let id = make_id(0x10);
686 let shard = table.shard_for(&id);
687 table.insert(make_entry(id, "a", 0, 0, shard), 0);
688 assert_eq!(table.total_insertions, 1);
689 }
690
691 #[test]
692 fn remove_existing_entry() {
693 let mut table = default_table();
694 let id = make_id(0x20);
695 let shard = table.shard_for(&id);
696 table.insert(make_entry(id, "a", 0, 0, shard), 0);
697 assert!(table.remove(&id));
698 assert!(table.get(&id).is_none());
699 }
700
701 #[test]
702 fn remove_absent_entry_returns_false() {
703 let mut table = default_table();
704 assert!(!table.remove(&make_id(0x30)));
705 }
706
707 #[test]
708 fn total_entries_reflects_inserts_and_removes() {
709 let mut table = default_table();
710 let ids: Vec<NodeId> = (0..5).map(|i| make_id(i * 16)).collect();
711 for &id in &ids {
712 let shard = table.shard_for(&id);
713 table.insert(make_entry(id, "x", 0, 0, shard), 0);
714 }
715 assert_eq!(table.total_entries(), 5);
716 table.remove(&ids[0]);
717 assert_eq!(table.total_entries(), 4);
718 }
719
720 #[test]
721 fn duplicate_insert_overwrites_existing_entry() {
722 let mut table = default_table();
723 let id = make_id(0x01);
724 let shard = table.shard_for(&id);
725 table.insert(make_entry(id, "old", 100, 10, shard), 100);
726 table.insert(make_entry(id, "new", 200, 20, shard), 200);
727 let got = table.get(&id).expect("entry present");
728 assert_eq!(got.addr, "new");
729 assert_eq!(got.rtt_ms, 20);
730 }
731
732 #[test]
737 fn update_rtt_existing_entry() {
738 let mut table = default_table();
739 let id = make_id(0x02);
740 let shard = table.shard_for(&id);
741 table.insert(make_entry(id, "a", 1000, 50, shard), 1000);
742 let ok = table.update_rtt(&id, 25, 2000);
743 assert!(ok);
744 let e = table.get(&id).expect("still present");
745 assert_eq!(e.rtt_ms, 25);
746 assert_eq!(e.last_seen, 2000);
747 }
748
749 #[test]
750 fn update_rtt_missing_entry_returns_false() {
751 let mut table = default_table();
752 assert!(!table.update_rtt(&make_id(0xFF), 10, 0));
753 }
754
755 #[test]
760 fn closest_nodes_returns_sorted_by_xor() {
761 let mut table = RoutingTableSharding::new(ShardConfig {
762 num_shards: 4,
763 max_entries_per_shard: 256,
764 eviction_policy: EvictionPolicy::LeastRecentlySeen,
765 });
766 let target = NodeId([0u8; 32]);
768 let ids: Vec<NodeId> = (1u8..=4).map(make_id).collect();
769 for &id in &ids {
770 let shard = table.shard_for(&id);
771 table.insert(make_entry(id, "a", 0, 0, shard), 0);
772 }
773 let closest = table.closest_nodes(&target, 2);
774 assert_eq!(closest.len(), 2);
775 assert_eq!(closest[0].node_id, make_id(1));
777 assert_eq!(closest[1].node_id, make_id(2));
778 }
779
780 #[test]
781 fn closest_nodes_k_larger_than_total_returns_all() {
782 let mut table = default_table();
783 for i in 0u8..5 {
784 let id = make_id(i);
785 let shard = table.shard_for(&id);
786 table.insert(make_entry(id, "a", 0, 0, shard), 0);
787 }
788 let result = table.closest_nodes(&NodeId([0u8; 32]), 100);
789 assert_eq!(result.len(), 5);
790 }
791
792 #[test]
793 fn closest_nodes_increments_lookup_counter() {
794 let table = default_table();
795 table.closest_nodes(&NodeId([0u8; 32]), 5);
796 assert_eq!(table.total_lookups.get(), 1);
797 }
798
799 #[test]
804 fn nodes_in_shard_returns_correct_entries() {
805 let mut table = RoutingTableSharding::new(ShardConfig {
806 num_shards: 4,
807 max_entries_per_shard: 256,
808 eviction_policy: EvictionPolicy::LeastRecentlySeen,
809 });
810 let id0 = make_id(0);
812 let id1 = make_id(1);
813 table.insert(make_entry(id0, "a", 0, 0, ShardId(0)), 0);
814 table.insert(make_entry(id1, "b", 0, 0, ShardId(1)), 0);
815 assert_eq!(table.nodes_in_shard(ShardId(0)).len(), 1);
816 assert_eq!(table.nodes_in_shard(ShardId(1)).len(), 1);
817 assert_eq!(table.nodes_in_shard(ShardId(2)).len(), 0);
818 }
819
820 #[test]
821 fn nodes_in_shard_invalid_shard_returns_empty() {
822 let table = default_table(); assert_eq!(table.nodes_in_shard(ShardId(200)).len(), 0);
824 }
825
826 #[test]
831 fn evict_stale_removes_old_entries() {
832 let mut table = default_table();
833 let id_old = make_id(0x00);
834 let id_new = make_id(0x10);
835 let s_old = table.shard_for(&id_old);
836 let s_new = table.shard_for(&id_new);
837 table.insert(make_entry(id_old, "old", 100, 0, s_old), 100);
838 table.insert(make_entry(id_new, "new", 2000, 0, s_new), 2000);
839
840 let removed = table.evict_stale(3000, 1500);
844 assert_eq!(removed, 1);
845 assert!(table.get(&id_old).is_none());
846 assert!(table.get(&id_new).is_some());
847 }
848
849 #[test]
850 fn evict_stale_no_stale_entries_returns_zero() {
851 let mut table = default_table();
852 let id = make_id(0x05);
853 let shard = table.shard_for(&id);
854 table.insert(make_entry(id, "a", 1000, 0, shard), 1000);
855 let removed = table.evict_stale(1500, 1000); assert_eq!(removed, 0);
857 }
858
859 #[test]
860 fn evict_stale_removes_all_stale() {
861 let mut table = default_table();
862 for i in 0u8..8 {
863 let id = make_id(i);
864 let shard = table.shard_for(&id);
865 table.insert(make_entry(id, "a", 0, 0, shard), 0);
866 }
867 let removed = table.evict_stale(10_000, 100); assert_eq!(removed, 8);
869 assert_eq!(table.total_entries(), 0);
870 }
871
872 #[test]
877 fn shard_stats_empty_shard() {
878 let table = default_table();
879 let stats = table.shard_stats(ShardId(0), 1000);
880 assert_eq!(stats.entry_count, 0);
881 assert_eq!(stats.avg_rtt_ms, 0.0);
882 assert_eq!(stats.oldest_entry_ms, 0);
883 }
884
885 #[test]
886 fn shard_stats_single_entry() {
887 let mut table = RoutingTableSharding::new(ShardConfig {
888 num_shards: 4,
889 max_entries_per_shard: 256,
890 eviction_policy: EvictionPolicy::LeastRecentlySeen,
891 });
892 let id = make_id(0); table.insert(make_entry(id, "a", 500, 20, ShardId(0)), 500);
894 let stats = table.shard_stats(ShardId(0), 1000);
895 assert_eq!(stats.entry_count, 1);
896 assert!((stats.avg_rtt_ms - 20.0).abs() < f64::EPSILON);
897 assert_eq!(stats.oldest_entry_ms, 500);
898 }
899
900 #[test]
901 fn shard_stats_avg_rtt_multiple_entries() {
902 let mut table = RoutingTableSharding::new(ShardConfig {
903 num_shards: 1,
904 max_entries_per_shard: 256,
905 eviction_policy: EvictionPolicy::LeastRecentlySeen,
906 });
907 let ids: Vec<NodeId> = (0u8..4).map(make_id).collect();
909 let rtts = [10u32, 20, 30, 40];
910 for (id, &rtt) in ids.iter().zip(rtts.iter()) {
911 table.insert(make_entry(*id, "a", 1000, rtt, ShardId(0)), 1000);
912 }
913 let stats = table.shard_stats(ShardId(0), 1000);
914 assert_eq!(stats.entry_count, 4);
915 assert!((stats.avg_rtt_ms - 25.0).abs() < 1e-9);
916 }
917
918 #[test]
919 fn all_stats_returns_one_per_shard_sorted() {
920 let table = default_table(); let stats = table.all_stats(1000);
922 assert_eq!(stats.len(), 16);
923 for (i, s) in stats.iter().enumerate() {
924 assert_eq!(s.shard_id, i as u8);
925 }
926 }
927
928 fn fill_shard_to_capacity(
933 table: &mut RoutingTableSharding,
934 shard_first_byte: u8,
935 count: usize,
936 ) {
937 let num_shards = table.config.num_shards;
941 for i in 0u8..(count as u8) {
942 let mut b = [0u8; 32];
943 b[0] = shard_first_byte;
944 b[1] = i;
945 let id = NodeId(b);
946 debug_assert_eq!(id.0[0] % num_shards, shard_first_byte % num_shards);
948 let shard = table.shard_for(&id);
949 let entry = RoutingEntry {
950 node_id: id,
951 addr: format!("10.0.0.{i}:4001"),
952 last_seen: i as u64 * 100, rtt_ms: 200 - i as u32, shard,
955 };
956 table.insert(entry, i as u64 * 100);
957 }
958 }
959
960 #[test]
961 fn eviction_lrs_removes_least_recently_seen() {
962 let max = 4usize;
963 let cfg = ShardConfig {
964 num_shards: 16,
965 max_entries_per_shard: max,
966 eviction_policy: EvictionPolicy::LeastRecentlySeen,
967 };
968 let mut table = RoutingTableSharding::new(cfg);
969 fill_shard_to_capacity(&mut table, 0, max);
972 assert_eq!(table.total_entries(), max);
973
974 let mut b = [0u8; 32];
976 b[0] = 0;
977 b[1] = 100; let trigger_id = NodeId(b);
979 let shard = table.shard_for(&trigger_id);
980 table.insert(
981 RoutingEntry {
982 node_id: trigger_id,
983 addr: "trigger".to_string(),
984 last_seen: 9999,
985 rtt_ms: 1,
986 shard,
987 },
988 9999,
989 );
990 assert_eq!(table.nodes_in_shard(shard).len(), max);
992 assert_eq!(table.total_evictions, 1);
993
994 let mut victim_b = [0u8; 32];
996 victim_b[0] = 0;
997 victim_b[1] = 0;
998 assert!(
999 table.get(&NodeId(victim_b)).is_none(),
1000 "LRS victim should be evicted"
1001 );
1002 }
1003
1004 #[test]
1005 fn eviction_highest_rtt_removes_slowest_peer() {
1006 let max = 4usize;
1007 let cfg = ShardConfig {
1008 num_shards: 16,
1009 max_entries_per_shard: max,
1010 eviction_policy: EvictionPolicy::HighestRtt,
1011 };
1012 let mut table = RoutingTableSharding::new(cfg);
1013 fill_shard_to_capacity(&mut table, 0, max);
1014
1015 let mut b = [0u8; 32];
1016 b[0] = 0;
1017 b[1] = 100;
1018 let trigger_id = NodeId(b);
1019 let shard = table.shard_for(&trigger_id);
1020 table.insert(
1021 RoutingEntry {
1022 node_id: trigger_id,
1023 addr: "trigger".to_string(),
1024 last_seen: 9999,
1025 rtt_ms: 1,
1026 shard,
1027 },
1028 9999,
1029 );
1030 assert_eq!(table.total_evictions, 1);
1031
1032 let mut victim_b = [0u8; 32];
1034 victim_b[0] = 0;
1035 victim_b[1] = 0;
1036 assert!(
1037 table.get(&NodeId(victim_b)).is_none(),
1038 "HighestRtt victim should be evicted"
1039 );
1040 }
1041
1042 #[test]
1043 fn eviction_random_removes_one_entry() {
1044 let max = 4usize;
1045 let cfg = ShardConfig {
1046 num_shards: 16,
1047 max_entries_per_shard: max,
1048 eviction_policy: EvictionPolicy::Random {
1049 seed: 0xDEAD_BEEF_1234_5678,
1050 },
1051 };
1052 let mut table = RoutingTableSharding::new(cfg);
1053 fill_shard_to_capacity(&mut table, 0, max);
1054
1055 let mut b = [0u8; 32];
1056 b[0] = 0;
1057 b[1] = 100;
1058 let trigger_id = NodeId(b);
1059 let shard = table.shard_for(&trigger_id);
1060 let evicted = table.insert(
1061 RoutingEntry {
1062 node_id: trigger_id,
1063 addr: "trigger".to_string(),
1064 last_seen: 9999,
1065 rtt_ms: 1,
1066 shard,
1067 },
1068 9999,
1069 );
1070 assert!(evicted, "Should have evicted one entry");
1071 assert_eq!(table.nodes_in_shard(ShardId(0)).len(), max);
1072 assert_eq!(table.total_evictions, 1);
1073 }
1074
1075 #[test]
1076 fn eviction_random_seed_zero_replaced_with_one() {
1077 let cfg = ShardConfig {
1079 num_shards: 1,
1080 max_entries_per_shard: 2,
1081 eviction_policy: EvictionPolicy::Random { seed: 0 },
1082 };
1083 let mut table = RoutingTableSharding::new(cfg);
1084 for i in 0u8..3 {
1085 let id = make_id(i);
1086 let shard = table.shard_for(&id);
1087 table.insert(make_entry(id, "a", i as u64 * 10, 10, shard), i as u64 * 10);
1088 }
1089 assert_eq!(table.total_entries(), 2);
1090 assert_eq!(table.total_evictions, 1);
1091 }
1092
1093 #[test]
1098 fn global_stats_tracks_all_counters() {
1099 let mut table = default_table();
1100 let id = make_id(0x04);
1101 let shard = table.shard_for(&id);
1102 table.insert(make_entry(id, "a", 0, 0, shard), 0);
1103 let _ = table.get(&id);
1104 table.closest_nodes(&NodeId([0u8; 32]), 10);
1105 let (ins, evic, look) = table.global_stats();
1106 assert_eq!(ins, 1);
1107 assert_eq!(evic, 0);
1108 assert_eq!(look, 2); }
1110
1111 #[test]
1116 fn empty_table_closest_nodes_returns_empty() {
1117 let table = default_table();
1118 assert!(table.closest_nodes(&NodeId([0u8; 32]), 10).is_empty());
1119 }
1120
1121 #[test]
1122 fn empty_table_evict_stale_returns_zero() {
1123 let mut table = default_table();
1124 assert_eq!(table.evict_stale(9999, 0), 0);
1125 }
1126
1127 #[test]
1128 fn large_insertion_stress() {
1129 let cfg = ShardConfig {
1130 num_shards: 8,
1131 max_entries_per_shard: 50,
1132 eviction_policy: EvictionPolicy::LeastRecentlySeen,
1133 };
1134 let mut table = RoutingTableSharding::new(cfg);
1135 for i in 0u32..1000 {
1136 let mut b = [0u8; 32];
1137 b[0] = (i % 256) as u8;
1138 b[1] = (i / 256) as u8;
1139 let id = NodeId(b);
1140 let shard = table.shard_for(&id);
1141 table.insert(
1142 RoutingEntry {
1143 node_id: id,
1144 addr: "x".to_string(),
1145 last_seen: i as u64,
1146 rtt_ms: 1,
1147 shard,
1148 },
1149 i as u64,
1150 );
1151 }
1152 for shard_map in &table.shards {
1154 assert!(
1155 shard_map.len() <= 50,
1156 "Shard overflow: {} > 50",
1157 shard_map.len()
1158 );
1159 }
1160 assert!(
1161 table.total_evictions > 0,
1162 "Expected some evictions under stress"
1163 );
1164 }
1165
1166 #[test]
1167 fn node_id_xor_distance_commutativity() {
1168 let a = make_id_full({
1169 let mut b = [0u8; 32];
1170 b[0] = 0xAB;
1171 b[15] = 0xCD;
1172 b
1173 });
1174 let b = make_id_full({
1175 let mut b = [0u8; 32];
1176 b[0] = 0x12;
1177 b[15] = 0x34;
1178 b
1179 });
1180 assert_eq!(a.xor_distance(&b), b.xor_distance(&a));
1181 }
1182
1183 #[test]
1184 fn multiple_evictions_counted_correctly() {
1185 let cfg = ShardConfig {
1186 num_shards: 1,
1187 max_entries_per_shard: 3,
1188 eviction_policy: EvictionPolicy::HighestRtt,
1189 };
1190 let mut table = RoutingTableSharding::new(cfg);
1191 for i in 0u8..10 {
1192 let id = make_id(i);
1193 let shard = table.shard_for(&id);
1194 table.insert(make_entry(id, "a", i as u64, i as u32, shard), i as u64);
1195 }
1196 assert_eq!(table.total_evictions, 7); assert_eq!(table.total_entries(), 3);
1198 }
1199
1200 #[test]
1201 fn update_rtt_does_not_change_shard_assignment() {
1202 let mut table = default_table();
1203 let id = make_id(0x07);
1204 let shard_before = table.shard_for(&id);
1205 table.insert(make_entry(id, "a", 0, 10, shard_before), 0);
1206 table.update_rtt(&id, 99, 500);
1207 let entry = table.get(&id).expect("still present");
1208 assert_eq!(entry.shard, shard_before);
1209 }
1210
1211 #[test]
1212 fn closest_nodes_across_multiple_shards() {
1213 let mut table = RoutingTableSharding::new(ShardConfig {
1214 num_shards: 16,
1215 max_entries_per_shard: 256,
1216 eviction_policy: EvictionPolicy::LeastRecentlySeen,
1217 });
1218 for i in 0u8..32 {
1220 let id = make_id(i);
1221 let shard = table.shard_for(&id);
1222 table.insert(make_entry(id, "a", 0, 0, shard), 0);
1223 }
1224 let target = NodeId([0u8; 32]);
1225 let result = table.closest_nodes(&target, 5);
1226 assert_eq!(result.len(), 5);
1227 for w in result.windows(2) {
1229 let d0 = w[0].node_id.xor_distance(&target);
1230 let d1 = w[1].node_id.xor_distance(&target);
1231 assert!(d0 <= d1);
1232 }
1233 }
1234}