1use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ReplicationError {
15 NodeAlreadyRegistered(String),
17 NodeNotFound(String),
19 CidNotFound(String),
21}
22
23impl std::fmt::Display for ReplicationError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::NodeAlreadyRegistered(id) => write!(f, "node already registered: {id}"),
27 Self::NodeNotFound(id) => write!(f, "node not found: {id}"),
28 Self::CidNotFound(cid) => write!(f, "CID not found: {cid}"),
29 }
30 }
31}
32
33impl std::error::Error for ReplicationError {}
34
35#[derive(Debug, Clone)]
39pub struct ReplicaNode {
40 pub node_id: String,
42 pub address: String,
44 pub last_seen: u64,
46 pub healthy: bool,
48 pub capacity_bytes: u64,
50 pub used_bytes: u64,
52}
53
54impl ReplicaNode {
55 pub fn available_bytes(&self) -> u64 {
57 self.capacity_bytes.saturating_sub(self.used_bytes)
58 }
59
60 pub fn utilization(&self) -> f64 {
62 self.used_bytes as f64 / self.capacity_bytes.max(1) as f64
63 }
64}
65
66#[derive(Debug, Clone)]
70pub struct RmReplicaLocation {
71 pub node_id: String,
73 pub cid: String,
75 pub stored_at: u64,
77 pub verified_at: Option<u64>,
79 pub checksum: u64,
81}
82
83#[derive(Debug, Clone)]
87pub struct RmReplicationPolicy {
88 pub replication_factor: u32,
90 pub min_healthy_replicas: u32,
92 pub prefer_local: bool,
94 pub max_node_utilization: f64,
96}
97
98impl Default for RmReplicationPolicy {
99 fn default() -> Self {
100 Self {
101 replication_factor: 3,
102 min_healthy_replicas: 2,
103 prefer_local: false,
104 max_node_utilization: 0.85,
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum RmReplicationStatus {
114 Healthy {
116 replicas: u32,
118 },
119 UnderReplicated {
121 current: u32,
123 target: u32,
125 },
126 OverReplicated {
128 current: u32,
130 target: u32,
132 },
133 Missing,
135}
136
137#[derive(Debug, Clone)]
141pub struct RmReplicationStats {
142 pub total_cids: usize,
144 pub healthy_cids: usize,
146 pub under_replicated_cids: usize,
148 pub over_replicated_cids: usize,
150 pub missing_cids: usize,
152 pub total_nodes: usize,
154 pub healthy_nodes: usize,
156 pub avg_replication_factor: f64,
158}
159
160pub struct StorageReplicationManager {
168 pub policy: RmReplicationPolicy,
170 pub nodes: HashMap<String, ReplicaNode>,
172 pub replicas: HashMap<String, Vec<RmReplicaLocation>>,
174}
175
176impl StorageReplicationManager {
177 pub fn new(policy: RmReplicationPolicy) -> Self {
179 Self {
180 policy,
181 nodes: HashMap::new(),
182 replicas: HashMap::new(),
183 }
184 }
185
186 pub fn register_node(&mut self, node: ReplicaNode) -> Result<(), ReplicationError> {
193 if self.nodes.contains_key(&node.node_id) {
194 return Err(ReplicationError::NodeAlreadyRegistered(
195 node.node_id.clone(),
196 ));
197 }
198 self.nodes.insert(node.node_id.clone(), node);
199 Ok(())
200 }
201
202 pub fn deregister_node(&mut self, node_id: &str) -> Result<(), ReplicationError> {
206 if self.nodes.remove(node_id).is_none() {
207 return Err(ReplicationError::NodeNotFound(node_id.to_string()));
208 }
209 for locations in self.replicas.values_mut() {
211 locations.retain(|loc| loc.node_id != node_id);
212 }
213 self.replicas.retain(|_, locs| !locs.is_empty());
215 Ok(())
216 }
217
218 pub fn mark_healthy(&mut self, node_id: &str, now: u64) -> bool {
222 if let Some(node) = self.nodes.get_mut(node_id) {
223 node.healthy = true;
224 node.last_seen = now;
225 true
226 } else {
227 false
228 }
229 }
230
231 pub fn mark_unhealthy(&mut self, node_id: &str) -> bool {
235 if let Some(node) = self.nodes.get_mut(node_id) {
236 node.healthy = false;
237 true
238 } else {
239 false
240 }
241 }
242
243 pub fn record_replica(
249 &mut self,
250 cid: String,
251 node_id: &str,
252 checksum: u64,
253 now: u64,
254 ) -> Result<(), ReplicationError> {
255 if !self.nodes.contains_key(node_id) {
256 return Err(ReplicationError::NodeNotFound(node_id.to_string()));
257 }
258 let location = RmReplicaLocation {
259 node_id: node_id.to_string(),
260 cid: cid.clone(),
261 stored_at: now,
262 verified_at: None,
263 checksum,
264 };
265 self.replicas.entry(cid).or_default().push(location);
266 Ok(())
267 }
268
269 pub fn verify_replica(&mut self, cid: &str, node_id: &str, now: u64) -> bool {
273 if let Some(locations) = self.replicas.get_mut(cid) {
274 for loc in locations.iter_mut() {
275 if loc.node_id == node_id {
276 loc.verified_at = Some(now);
277 return true;
278 }
279 }
280 }
281 false
282 }
283
284 pub fn remove_replica(&mut self, cid: &str, node_id: &str) -> bool {
288 if let Some(locations) = self.replicas.get_mut(cid) {
289 let before = locations.len();
290 locations.retain(|loc| loc.node_id != node_id);
291 let removed = locations.len() < before;
292 if locations.is_empty() {
293 self.replicas.remove(cid);
294 }
295 removed
296 } else {
297 false
298 }
299 }
300
301 pub fn replication_status(&self, cid: &str) -> RmReplicationStatus {
308 let Some(locations) = self.replicas.get(cid) else {
309 return RmReplicationStatus::Missing;
310 };
311
312 let healthy_count = self.count_healthy_replicas(locations);
313 let target = self.policy.replication_factor;
314 let min_healthy = self.policy.min_healthy_replicas;
315
316 if healthy_count == 0 {
317 RmReplicationStatus::UnderReplicated { current: 0, target }
319 } else if healthy_count > target {
320 RmReplicationStatus::OverReplicated {
321 current: healthy_count,
322 target,
323 }
324 } else if healthy_count >= min_healthy {
325 RmReplicationStatus::Healthy {
326 replicas: healthy_count,
327 }
328 } else {
329 RmReplicationStatus::UnderReplicated {
330 current: healthy_count,
331 target,
332 }
333 }
334 }
335
336 pub fn under_replicated_cids(&self) -> Vec<&str> {
338 let mut result: Vec<&str> = self
339 .all_known_cids()
340 .filter(|cid| {
341 matches!(
342 self.replication_status(cid),
343 RmReplicationStatus::UnderReplicated { .. } | RmReplicationStatus::Missing
344 )
345 })
346 .collect();
347 result.sort_unstable();
348 result
349 }
350
351 pub fn over_replicated_cids(&self) -> Vec<&str> {
353 let mut result: Vec<&str> = self
354 .all_known_cids()
355 .filter(|cid| {
356 matches!(
357 self.replication_status(cid),
358 RmReplicationStatus::OverReplicated { .. }
359 )
360 })
361 .collect();
362 result.sort_unstable();
363 result
364 }
365
366 pub fn select_nodes_for_replication(&self, cid: &str, count: usize) -> Vec<&ReplicaNode> {
375 let already_has: std::collections::HashSet<&str> = self
377 .replicas
378 .get(cid)
379 .map(|locs| locs.iter().map(|l| l.node_id.as_str()).collect())
380 .unwrap_or_default();
381
382 let max_util = self.policy.max_node_utilization;
383
384 let mut candidates: Vec<&ReplicaNode> = self
385 .nodes
386 .values()
387 .filter(|n| {
388 n.healthy
389 && !already_has.contains(n.node_id.as_str())
390 && n.utilization() <= max_util
391 })
392 .collect();
393
394 candidates.sort_by_key(|n| std::cmp::Reverse(n.available_bytes()));
396 candidates.truncate(count);
397 candidates
398 }
399
400 pub fn evict_stale_nodes(&mut self, max_age_ms: u64, now: u64) -> usize {
407 let stale_ids: Vec<String> = self
408 .nodes
409 .iter()
410 .filter(|(_, n)| now.saturating_sub(n.last_seen) > max_age_ms)
411 .map(|(id, _)| id.clone())
412 .collect();
413
414 let count = stale_ids.len();
415 for id in &stale_ids {
416 self.nodes.remove(id);
417 for locations in self.replicas.values_mut() {
418 locations.retain(|loc| &loc.node_id != id);
419 }
420 }
421 self.replicas.retain(|_, locs| !locs.is_empty());
422 count
423 }
424
425 pub fn cid_count(&self) -> usize {
429 self.replicas.len()
430 }
431
432 pub fn node_count(&self) -> usize {
434 self.nodes.len()
435 }
436
437 pub fn healthy_node_count(&self) -> usize {
439 self.nodes.values().filter(|n| n.healthy).count()
440 }
441
442 pub fn stats(&self) -> RmReplicationStats {
446 let total_cids = self.cid_count();
447 let total_nodes = self.node_count();
448 let healthy_nodes = self.healthy_node_count();
449
450 let mut healthy_cids = 0usize;
451 let mut under_replicated_cids = 0usize;
452 let mut over_replicated_cids = 0usize;
453 let mut missing_cids = 0usize;
454 let mut total_healthy_replicas = 0u64;
455
456 for cid in self.replicas.keys() {
457 match self.replication_status(cid) {
458 RmReplicationStatus::Healthy { replicas } => {
459 healthy_cids += 1;
460 total_healthy_replicas += u64::from(replicas);
461 }
462 RmReplicationStatus::UnderReplicated { current, .. } => {
463 under_replicated_cids += 1;
464 total_healthy_replicas += u64::from(current);
465 }
466 RmReplicationStatus::OverReplicated { current, .. } => {
467 over_replicated_cids += 1;
468 total_healthy_replicas += u64::from(current);
469 }
470 RmReplicationStatus::Missing => {
471 missing_cids += 1;
472 }
473 }
474 }
475
476 let avg_replication_factor = if total_cids == 0 {
477 0.0
478 } else {
479 total_healthy_replicas as f64 / total_cids as f64
480 };
481
482 RmReplicationStats {
483 total_cids,
484 healthy_cids,
485 under_replicated_cids,
486 over_replicated_cids,
487 missing_cids,
488 total_nodes,
489 healthy_nodes,
490 avg_replication_factor,
491 }
492 }
493
494 fn count_healthy_replicas(&self, locations: &[RmReplicaLocation]) -> u32 {
497 locations
498 .iter()
499 .filter(|loc| {
500 self.nodes
501 .get(&loc.node_id)
502 .map(|n| n.healthy)
503 .unwrap_or(false)
504 })
505 .count() as u32
506 }
507
508 fn all_known_cids(&self) -> impl Iterator<Item = &str> {
509 self.replicas.keys().map(|s| s.as_str())
510 }
511}
512
513#[derive(Debug, Clone)]
518pub struct ReplicationConfig {
519 pub default_replica_count: usize,
521 pub max_attempts: u32,
523 pub retry_interval_ticks: u64,
525}
526
527impl Default for ReplicationConfig {
528 fn default() -> Self {
529 Self {
530 default_replica_count: 3,
531 max_attempts: 5,
532 retry_interval_ticks: 10,
533 }
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum ReplicationState {
540 Pending,
542 InProgress,
544 Completed,
546 Failed,
548}
549
550#[derive(Debug, Clone)]
552pub struct ReplicaInfo {
553 pub peer_id: String,
555 pub state: ReplicationState,
557 pub replicated_tick: Option<u64>,
559 pub attempts: u32,
561}
562
563#[derive(Debug, Clone)]
565pub struct BlockReplicas {
566 pub block_cid: String,
568 pub desired_replicas: usize,
570 pub replicas: Vec<ReplicaInfo>,
572}
573
574#[derive(Debug, Clone)]
576pub struct ReplicationManagerStats {
577 pub tracked_blocks: usize,
579 pub total_replications: u64,
581 pub total_failures: u64,
583 pub under_replicated: usize,
585 pub fully_replicated: usize,
587}
588
589#[cfg(test)]
592mod tests {
593 use crate::replication_manager::{
594 BlockReplicas, ReplicaInfo, ReplicaNode, ReplicationConfig, ReplicationError,
595 ReplicationManagerStats, ReplicationState, RmReplicaLocation, RmReplicationPolicy,
596 RmReplicationStatus, StorageReplicationManager,
597 };
598
599 fn default_policy() -> RmReplicationPolicy {
602 RmReplicationPolicy::default()
603 }
604
605 fn make_manager() -> StorageReplicationManager {
606 StorageReplicationManager::new(default_policy())
607 }
608
609 fn healthy_node(id: &str, capacity: u64, used: u64) -> ReplicaNode {
610 ReplicaNode {
611 node_id: id.to_string(),
612 address: format!("192.168.0.1:{id}"),
613 last_seen: 1000,
614 healthy: true,
615 capacity_bytes: capacity,
616 used_bytes: used,
617 }
618 }
619
620 fn unhealthy_node(id: &str) -> ReplicaNode {
621 ReplicaNode {
622 node_id: id.to_string(),
623 address: format!("192.168.0.2:{id}"),
624 last_seen: 500,
625 healthy: false,
626 capacity_bytes: 1_000_000,
627 used_bytes: 100_000,
628 }
629 }
630
631 #[test]
634 fn test_replica_node_available_bytes() {
635 let n = healthy_node("n1", 1_000, 300);
636 assert_eq!(n.available_bytes(), 700);
637 }
638
639 #[test]
640 fn test_replica_node_available_bytes_saturates() {
641 let n = ReplicaNode {
642 node_id: "n".into(),
643 address: "a".into(),
644 last_seen: 0,
645 healthy: true,
646 capacity_bytes: 100,
647 used_bytes: 200,
648 };
649 assert_eq!(n.available_bytes(), 0);
650 }
651
652 #[test]
653 fn test_replica_node_utilization() {
654 let n = healthy_node("n1", 1_000, 500);
655 assert!((n.utilization() - 0.5).abs() < f64::EPSILON);
656 }
657
658 #[test]
659 fn test_replica_node_utilization_zero_capacity() {
660 let n = ReplicaNode {
661 node_id: "n".into(),
662 address: "a".into(),
663 last_seen: 0,
664 healthy: true,
665 capacity_bytes: 0,
666 used_bytes: 0,
667 };
668 assert!((n.utilization() - 0.0).abs() < f64::EPSILON);
670 }
671
672 #[test]
675 fn test_register_node_success() {
676 let mut mgr = make_manager();
677 let result = mgr.register_node(healthy_node("n1", 1_000_000, 0));
678 assert!(result.is_ok());
679 assert_eq!(mgr.node_count(), 1);
680 }
681
682 #[test]
683 fn test_register_node_duplicate_error() {
684 let mut mgr = make_manager();
685 mgr.register_node(healthy_node("n1", 1_000_000, 0))
686 .unwrap_or_default();
687 let err = mgr
688 .register_node(healthy_node("n1", 500_000, 0))
689 .unwrap_err();
690 assert_eq!(err, ReplicationError::NodeAlreadyRegistered("n1".into()));
691 }
692
693 #[test]
694 fn test_deregister_node_success() {
695 let mut mgr = make_manager();
696 mgr.register_node(healthy_node("n1", 1_000_000, 0))
697 .unwrap_or_default();
698 let result = mgr.deregister_node("n1");
699 assert!(result.is_ok());
700 assert_eq!(mgr.node_count(), 0);
701 }
702
703 #[test]
704 fn test_deregister_node_not_found() {
705 let mut mgr = make_manager();
706 let err = mgr.deregister_node("ghost").unwrap_err();
707 assert_eq!(err, ReplicationError::NodeNotFound("ghost".into()));
708 }
709
710 #[test]
711 fn test_deregister_node_removes_replicas() {
712 let mut mgr = make_manager();
713 mgr.register_node(healthy_node("n1", 1_000_000, 0))
714 .unwrap_or_default();
715 mgr.record_replica("cid1".into(), "n1", 42, 100)
716 .unwrap_or_default();
717 assert_eq!(mgr.cid_count(), 1);
718 mgr.deregister_node("n1").unwrap_or_default();
719 assert_eq!(mgr.cid_count(), 0);
721 }
722
723 #[test]
726 fn test_mark_healthy_updates_node() {
727 let mut mgr = make_manager();
728 mgr.register_node(unhealthy_node("n1")).unwrap_or_default();
729 assert!(mgr.mark_healthy("n1", 9999));
730 let n = mgr.nodes.get("n1").expect("node must exist");
731 assert!(n.healthy);
732 assert_eq!(n.last_seen, 9999);
733 }
734
735 #[test]
736 fn test_mark_healthy_returns_false_unknown() {
737 let mut mgr = make_manager();
738 assert!(!mgr.mark_healthy("nope", 1));
739 }
740
741 #[test]
742 fn test_mark_unhealthy() {
743 let mut mgr = make_manager();
744 mgr.register_node(healthy_node("n1", 1_000_000, 0))
745 .unwrap_or_default();
746 assert!(mgr.mark_unhealthy("n1"));
747 let n = mgr.nodes.get("n1").expect("node must exist");
748 assert!(!n.healthy);
749 }
750
751 #[test]
752 fn test_mark_unhealthy_returns_false_unknown() {
753 let mut mgr = make_manager();
754 assert!(!mgr.mark_unhealthy("nope"));
755 }
756
757 #[test]
760 fn test_record_replica_success() {
761 let mut mgr = make_manager();
762 mgr.register_node(healthy_node("n1", 1_000_000, 0))
763 .unwrap_or_default();
764 let r = mgr.record_replica("cid1".into(), "n1", 0xdeadbeef, 200);
765 assert!(r.is_ok());
766 assert_eq!(mgr.cid_count(), 1);
767 }
768
769 #[test]
770 fn test_record_replica_node_not_found() {
771 let mut mgr = make_manager();
772 let err = mgr
773 .record_replica("cid1".into(), "ghost", 0, 0)
774 .unwrap_err();
775 assert_eq!(err, ReplicationError::NodeNotFound("ghost".into()));
776 }
777
778 #[test]
779 fn test_verify_replica_found() {
780 let mut mgr = make_manager();
781 mgr.register_node(healthy_node("n1", 1_000_000, 0))
782 .unwrap_or_default();
783 mgr.record_replica("cid1".into(), "n1", 0, 100)
784 .unwrap_or_default();
785 assert!(mgr.verify_replica("cid1", "n1", 300));
786 let loc = &mgr.replicas["cid1"][0];
787 assert_eq!(loc.verified_at, Some(300));
788 }
789
790 #[test]
791 fn test_verify_replica_not_found() {
792 let mut mgr = make_manager();
793 assert!(!mgr.verify_replica("cid_missing", "n1", 0));
794 }
795
796 #[test]
797 fn test_remove_replica_success() {
798 let mut mgr = make_manager();
799 mgr.register_node(healthy_node("n1", 1_000_000, 0))
800 .unwrap_or_default();
801 mgr.record_replica("cid1".into(), "n1", 0, 100)
802 .unwrap_or_default();
803 assert!(mgr.remove_replica("cid1", "n1"));
804 assert_eq!(mgr.cid_count(), 0);
805 }
806
807 #[test]
808 fn test_remove_replica_not_found() {
809 let mut mgr = make_manager();
810 assert!(!mgr.remove_replica("cid_missing", "n1"));
811 }
812
813 #[test]
816 fn test_status_missing_no_cid() {
817 let mgr = make_manager();
818 assert_eq!(mgr.replication_status("cid1"), RmReplicationStatus::Missing);
819 }
820
821 #[test]
822 fn test_status_under_replicated_no_healthy_nodes() {
823 let mut mgr = make_manager();
824 mgr.register_node(unhealthy_node("n1")).unwrap_or_default();
825 mgr.record_replica("cid1".into(), "n1", 0, 100)
826 .unwrap_or_default();
827 assert_eq!(
828 mgr.replication_status("cid1"),
829 RmReplicationStatus::UnderReplicated {
830 current: 0,
831 target: 3
832 }
833 );
834 }
835
836 #[test]
837 fn test_status_healthy() {
838 let mut mgr = make_manager();
839 for i in 0..2u8 {
840 let id = format!("n{i}");
841 mgr.register_node(healthy_node(&id, 1_000_000, 0))
842 .unwrap_or_default();
843 mgr.record_replica("cid1".into(), &id, 0, 100)
844 .unwrap_or_default();
845 }
846 assert_eq!(
847 mgr.replication_status("cid1"),
848 RmReplicationStatus::Healthy { replicas: 2 }
849 );
850 }
851
852 #[test]
853 fn test_status_over_replicated() {
854 let mut mgr = StorageReplicationManager::new(RmReplicationPolicy {
855 replication_factor: 2,
856 min_healthy_replicas: 1,
857 ..Default::default()
858 });
859 for i in 0..3u8 {
860 let id = format!("n{i}");
861 mgr.register_node(healthy_node(&id, 1_000_000, 0))
862 .unwrap_or_default();
863 mgr.record_replica("cid1".into(), &id, 0, 100)
864 .unwrap_or_default();
865 }
866 assert_eq!(
867 mgr.replication_status("cid1"),
868 RmReplicationStatus::OverReplicated {
869 current: 3,
870 target: 2
871 }
872 );
873 }
874
875 #[test]
878 fn test_under_replicated_cids_sorted() {
879 let mut mgr = make_manager();
880 mgr.register_node(unhealthy_node("n1")).unwrap_or_default();
883 mgr.record_replica("cid_b".into(), "n1", 0, 1)
884 .unwrap_or_default();
885 mgr.record_replica("cid_a".into(), "n1", 0, 1)
886 .unwrap_or_default();
887 let under = mgr.under_replicated_cids();
888 assert_eq!(under, vec!["cid_a", "cid_b"]);
889 }
890
891 #[test]
892 fn test_over_replicated_cids_sorted() {
893 let mut mgr = StorageReplicationManager::new(RmReplicationPolicy {
894 replication_factor: 1,
895 min_healthy_replicas: 1,
896 ..Default::default()
897 });
898 for suffix in ["z", "a", "m"] {
899 let cid = format!("cid_{suffix}");
900 for i in 0..2u8 {
901 let node_id = format!("n{suffix}{i}");
902 mgr.register_node(healthy_node(&node_id, 1_000_000, 0))
903 .unwrap_or_default();
904 mgr.record_replica(cid.clone(), &node_id, 0, 1)
905 .unwrap_or_default();
906 }
907 }
908 let over = mgr.over_replicated_cids();
909 assert_eq!(over, vec!["cid_a", "cid_m", "cid_z"]);
910 }
911
912 #[test]
915 fn test_select_excludes_existing_replica_node() {
916 let mut mgr = make_manager();
917 mgr.register_node(healthy_node("n1", 1_000_000, 0))
918 .unwrap_or_default();
919 mgr.register_node(healthy_node("n2", 1_000_000, 0))
920 .unwrap_or_default();
921 mgr.record_replica("cid1".into(), "n1", 0, 1)
922 .unwrap_or_default();
923 let selected = mgr.select_nodes_for_replication("cid1", 10);
924 assert_eq!(selected.len(), 1);
925 assert_eq!(selected[0].node_id, "n2");
926 }
927
928 #[test]
929 fn test_select_excludes_unhealthy_nodes() {
930 let mut mgr = make_manager();
931 mgr.register_node(healthy_node("n1", 1_000_000, 0))
932 .unwrap_or_default();
933 mgr.register_node(unhealthy_node("n2")).unwrap_or_default();
934 let selected = mgr.select_nodes_for_replication("cid1", 10);
935 assert_eq!(selected.len(), 1);
936 assert_eq!(selected[0].node_id, "n1");
937 }
938
939 #[test]
940 fn test_select_excludes_over_utilized_nodes() {
941 let mut mgr = make_manager();
942 mgr.register_node(healthy_node("n1", 1_000_000, 900_000))
944 .unwrap_or_default();
945 mgr.register_node(healthy_node("n2", 1_000_000, 100_000))
946 .unwrap_or_default();
947 let selected = mgr.select_nodes_for_replication("cid1", 10);
948 assert_eq!(selected.len(), 1);
949 assert_eq!(selected[0].node_id, "n2");
950 }
951
952 #[test]
953 fn test_select_sorted_by_available_bytes_desc() {
954 let mut mgr = make_manager();
955 mgr.register_node(healthy_node("n1", 1_000_000, 800_000))
956 .unwrap_or_default(); mgr.register_node(healthy_node("n2", 1_000_000, 100_000))
958 .unwrap_or_default(); mgr.register_node(healthy_node("n3", 1_000_000, 500_000))
960 .unwrap_or_default(); let selected = mgr.select_nodes_for_replication("cid1", 3);
962 assert_eq!(selected.len(), 3);
963 assert_eq!(selected[0].node_id, "n2");
964 assert_eq!(selected[1].node_id, "n3");
965 assert_eq!(selected[2].node_id, "n1");
966 }
967
968 #[test]
969 fn test_select_count_limits_result() {
970 let mut mgr = make_manager();
971 for i in 0..5u8 {
972 mgr.register_node(healthy_node(&format!("n{i}"), 1_000_000, 0))
973 .unwrap_or_default();
974 }
975 let selected = mgr.select_nodes_for_replication("cid1", 2);
976 assert_eq!(selected.len(), 2);
977 }
978
979 #[test]
980 fn test_select_returns_empty_when_no_eligible() {
981 let mut mgr = make_manager();
982 mgr.register_node(unhealthy_node("n1")).unwrap_or_default();
983 let selected = mgr.select_nodes_for_replication("cid1", 5);
984 assert!(selected.is_empty());
985 }
986
987 #[test]
990 fn test_evict_stale_nodes_removes_old() {
991 let mut mgr = make_manager();
992 mgr.register_node(ReplicaNode {
993 node_id: "old".into(),
994 address: "a".into(),
995 last_seen: 100,
996 healthy: true,
997 capacity_bytes: 1_000_000,
998 used_bytes: 0,
999 })
1000 .unwrap_or_default();
1001 mgr.register_node(ReplicaNode {
1002 node_id: "fresh".into(),
1003 address: "b".into(),
1004 last_seen: 9_000,
1005 healthy: true,
1006 capacity_bytes: 1_000_000,
1007 used_bytes: 0,
1008 })
1009 .unwrap_or_default();
1010 let evicted = mgr.evict_stale_nodes(1_000, 10_000);
1013 assert_eq!(evicted, 1);
1014 assert_eq!(mgr.node_count(), 1);
1015 assert!(mgr.nodes.contains_key("fresh"));
1016 }
1017
1018 #[test]
1019 fn test_evict_stale_cleans_replicas() {
1020 let mut mgr = make_manager();
1021 mgr.register_node(ReplicaNode {
1022 node_id: "old".into(),
1023 address: "a".into(),
1024 last_seen: 0,
1025 healthy: true,
1026 capacity_bytes: 1_000_000,
1027 used_bytes: 0,
1028 })
1029 .unwrap_or_default();
1030 mgr.record_replica("cid1".into(), "old", 0, 1)
1031 .unwrap_or_default();
1032 mgr.evict_stale_nodes(500, 10_000);
1033 assert_eq!(mgr.cid_count(), 0);
1034 }
1035
1036 #[test]
1037 fn test_evict_stale_none_old() {
1038 let mut mgr = make_manager();
1039 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1040 .unwrap_or_default();
1041 let evicted = mgr.evict_stale_nodes(10_000, 2_000);
1042 assert_eq!(evicted, 0);
1043 assert_eq!(mgr.node_count(), 1);
1044 }
1045
1046 #[test]
1049 fn test_node_count() {
1050 let mut mgr = make_manager();
1051 assert_eq!(mgr.node_count(), 0);
1052 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1053 .unwrap_or_default();
1054 mgr.register_node(healthy_node("n2", 1_000_000, 0))
1055 .unwrap_or_default();
1056 assert_eq!(mgr.node_count(), 2);
1057 }
1058
1059 #[test]
1060 fn test_healthy_node_count() {
1061 let mut mgr = make_manager();
1062 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1063 .unwrap_or_default();
1064 mgr.register_node(unhealthy_node("n2")).unwrap_or_default();
1065 assert_eq!(mgr.healthy_node_count(), 1);
1066 }
1067
1068 #[test]
1069 fn test_cid_count() {
1070 let mut mgr = make_manager();
1071 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1072 .unwrap_or_default();
1073 mgr.record_replica("cid1".into(), "n1", 0, 1)
1074 .unwrap_or_default();
1075 mgr.record_replica("cid2".into(), "n1", 0, 1)
1076 .unwrap_or_default();
1077 assert_eq!(mgr.cid_count(), 2);
1078 }
1079
1080 #[test]
1083 fn test_stats_empty() {
1084 let mgr = make_manager();
1085 let s = mgr.stats();
1086 assert_eq!(s.total_cids, 0);
1087 assert_eq!(s.total_nodes, 0);
1088 assert_eq!(s.healthy_nodes, 0);
1089 assert!((s.avg_replication_factor - 0.0).abs() < f64::EPSILON);
1090 }
1091
1092 #[test]
1093 fn test_stats_all_healthy() {
1094 let mut mgr = make_manager();
1095 for i in 0..3u8 {
1096 let id = format!("n{i}");
1097 mgr.register_node(healthy_node(&id, 1_000_000, 0))
1098 .unwrap_or_default();
1099 mgr.record_replica("cid1".into(), &id, 0, 1)
1100 .unwrap_or_default();
1101 }
1102 let s = mgr.stats();
1103 assert_eq!(s.total_cids, 1);
1104 assert_eq!(s.healthy_cids, 1);
1105 assert_eq!(s.under_replicated_cids, 0);
1106 assert_eq!(s.over_replicated_cids, 0);
1107 assert_eq!(s.missing_cids, 0);
1108 assert!((s.avg_replication_factor - 3.0).abs() < f64::EPSILON);
1109 }
1110
1111 #[test]
1112 fn test_stats_under_and_over() {
1113 let policy = RmReplicationPolicy {
1115 replication_factor: 2,
1116 min_healthy_replicas: 2,
1117 ..Default::default()
1118 };
1119 let mut mgr = StorageReplicationManager::new(policy);
1120 for i in 0..3u8 {
1122 let id = format!("over_n{i}");
1123 mgr.register_node(healthy_node(&id, 1_000_000, 0))
1124 .unwrap_or_default();
1125 mgr.record_replica("cid_over".into(), &id, 0, 1)
1126 .unwrap_or_default();
1127 }
1128 mgr.register_node(healthy_node("under_n0", 1_000_000, 0))
1130 .unwrap_or_default();
1131 mgr.record_replica("cid_under".into(), "under_n0", 0, 1)
1132 .unwrap_or_default();
1133
1134 let s = mgr.stats();
1135 assert_eq!(s.over_replicated_cids, 1);
1136 assert_eq!(s.under_replicated_cids, 1);
1137 assert_eq!(s.healthy_cids, 0);
1138 }
1139
1140 #[test]
1143 fn test_error_display_node_already_registered() {
1144 let e = ReplicationError::NodeAlreadyRegistered("n1".into());
1145 assert!(e.to_string().contains("n1"));
1146 }
1147
1148 #[test]
1149 fn test_error_display_node_not_found() {
1150 let e = ReplicationError::NodeNotFound("n2".into());
1151 assert!(e.to_string().contains("n2"));
1152 }
1153
1154 #[test]
1155 fn test_error_display_cid_not_found() {
1156 let e = ReplicationError::CidNotFound("cid_x".into());
1157 assert!(e.to_string().contains("cid_x"));
1158 }
1159
1160 #[test]
1163 fn test_policy_defaults() {
1164 let p = RmReplicationPolicy::default();
1165 assert_eq!(p.replication_factor, 3);
1166 assert_eq!(p.min_healthy_replicas, 2);
1167 assert!(!p.prefer_local);
1168 assert!((p.max_node_utilization - 0.85).abs() < f64::EPSILON);
1169 }
1170
1171 #[test]
1174 fn test_legacy_replication_config_default() {
1175 let cfg = ReplicationConfig::default();
1176 assert_eq!(cfg.default_replica_count, 3);
1177 assert_eq!(cfg.max_attempts, 5);
1178 assert_eq!(cfg.retry_interval_ticks, 10);
1179 }
1180
1181 #[test]
1182 fn test_legacy_block_replicas_fields() {
1183 let br = BlockReplicas {
1184 block_cid: "cid".into(),
1185 desired_replicas: 2,
1186 replicas: vec![ReplicaInfo {
1187 peer_id: "p".into(),
1188 state: ReplicationState::Pending,
1189 replicated_tick: None,
1190 attempts: 0,
1191 }],
1192 };
1193 assert_eq!(br.desired_replicas, 2);
1194 assert_eq!(br.replicas[0].state, ReplicationState::Pending);
1195 }
1196
1197 #[test]
1198 fn test_legacy_replication_manager_stats_fields() {
1199 let s = ReplicationManagerStats {
1200 tracked_blocks: 1,
1201 total_replications: 2,
1202 total_failures: 3,
1203 under_replicated: 4,
1204 fully_replicated: 5,
1205 };
1206 assert_eq!(s.tracked_blocks, 1);
1207 assert_eq!(s.fully_replicated, 5);
1208 }
1209
1210 #[test]
1213 fn test_deregister_leaves_other_cid_replicas() {
1214 let mut mgr = make_manager();
1215 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1216 .unwrap_or_default();
1217 mgr.register_node(healthy_node("n2", 1_000_000, 0))
1218 .unwrap_or_default();
1219 mgr.record_replica("cid1".into(), "n1", 0, 1)
1220 .unwrap_or_default();
1221 mgr.record_replica("cid1".into(), "n2", 0, 1)
1222 .unwrap_or_default();
1223 mgr.record_replica("cid2".into(), "n1", 0, 1)
1224 .unwrap_or_default();
1225 mgr.deregister_node("n1").unwrap_or_default();
1226 assert_eq!(mgr.cid_count(), 1);
1228 let locs = &mgr.replicas["cid1"];
1229 assert_eq!(locs.len(), 1);
1230 assert_eq!(locs[0].node_id, "n2");
1231 }
1232
1233 #[test]
1236 fn test_multiple_replicas_same_cid() {
1237 let mut mgr = make_manager();
1238 for i in 0..5u8 {
1239 let id = format!("n{i}");
1240 mgr.register_node(healthy_node(&id, 1_000_000, 0))
1241 .unwrap_or_default();
1242 mgr.record_replica("cid1".into(), &id, i as u64, 1)
1243 .unwrap_or_default();
1244 }
1245 assert_eq!(mgr.replicas["cid1"].len(), 5);
1246 }
1247
1248 #[test]
1251 fn test_verify_replica_updates_correct_location() {
1252 let mut mgr = make_manager();
1253 mgr.register_node(healthy_node("n1", 1_000_000, 0))
1254 .unwrap_or_default();
1255 mgr.register_node(healthy_node("n2", 1_000_000, 0))
1256 .unwrap_or_default();
1257 mgr.record_replica("cid1".into(), "n1", 0, 1)
1258 .unwrap_or_default();
1259 mgr.record_replica("cid1".into(), "n2", 0, 1)
1260 .unwrap_or_default();
1261 mgr.verify_replica("cid1", "n2", 999);
1262 let locs = &mgr.replicas["cid1"];
1263 let n1_loc = locs.iter().find(|l| l.node_id == "n1").expect("n1 loc");
1264 let n2_loc = locs.iter().find(|l| l.node_id == "n2").expect("n2 loc");
1265 assert_eq!(n1_loc.verified_at, None);
1266 assert_eq!(n2_loc.verified_at, Some(999));
1267 }
1268
1269 #[test]
1272 fn test_replica_location_fields() {
1273 let loc = RmReplicaLocation {
1274 node_id: "n1".into(),
1275 cid: "cid1".into(),
1276 stored_at: 100,
1277 verified_at: Some(200),
1278 checksum: 0xabcd,
1279 };
1280 assert_eq!(loc.checksum, 0xabcd);
1281 assert_eq!(loc.verified_at, Some(200));
1282 }
1283}