1use std::collections::{HashMap, VecDeque};
32
33const MAX_EVENT_LOG: usize = 500;
39
40#[derive(Clone, Debug)]
46pub enum ConnectionEvent {
47 Connected {
49 peer_id: String,
50 address: String,
51 at_secs: u64,
52 },
53 Disconnected {
55 peer_id: String,
56 reason: String,
57 at_secs: u64,
58 },
59 ProtocolNegotiated {
61 peer_id: String,
62 protocol: String,
63 at_secs: u64,
64 },
65 PingSuccess {
67 peer_id: String,
68 rtt_ms: f64,
69 at_secs: u64,
70 },
71 PingFailed { peer_id: String, at_secs: u64 },
73 AddressChanged {
75 peer_id: String,
76 new_address: String,
77 at_secs: u64,
78 },
79}
80
81impl ConnectionEvent {
82 pub fn peer_id(&self) -> &str {
84 match self {
85 ConnectionEvent::Connected { peer_id, .. } => peer_id,
86 ConnectionEvent::Disconnected { peer_id, .. } => peer_id,
87 ConnectionEvent::ProtocolNegotiated { peer_id, .. } => peer_id,
88 ConnectionEvent::PingSuccess { peer_id, .. } => peer_id,
89 ConnectionEvent::PingFailed { peer_id, .. } => peer_id,
90 ConnectionEvent::AddressChanged { peer_id, .. } => peer_id,
91 }
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum ConnectionState {
102 Connected,
104 Disconnected,
106 Connecting,
108}
109
110#[derive(Clone, Debug)]
116pub struct PeerConnectionInfo {
117 pub peer_id: String,
119 pub address: String,
121 pub state: ConnectionState,
123 pub connected_at_secs: Option<u64>,
125 pub disconnected_at_secs: Option<u64>,
127 pub protocols: Vec<String>,
129 pub ping_count: u64,
131 pub ping_failures: u64,
133 pub avg_rtt_ms: f64,
135}
136
137impl PeerConnectionInfo {
138 fn new(peer_id: String, address: String) -> Self {
140 Self {
141 peer_id,
142 address,
143 state: ConnectionState::Connecting,
144 connected_at_secs: None,
145 disconnected_at_secs: None,
146 protocols: Vec::new(),
147 ping_count: 0,
148 ping_failures: 0,
149 avg_rtt_ms: 0.0,
150 }
151 }
152
153 pub fn uptime_secs(&self, now_secs: u64) -> u64 {
157 if self.state != ConnectionState::Connected {
158 return 0;
159 }
160 match self.connected_at_secs {
161 Some(connected_at) if now_secs >= connected_at => now_secs - connected_at,
162 _ => 0,
163 }
164 }
165
166 pub fn reliability(&self) -> f64 {
168 1.0 - (self.ping_failures as f64 / self.ping_count.max(1) as f64)
169 }
170}
171
172#[derive(Clone, Debug, Default)]
178pub struct TrackerStats {
179 pub total_connections: u64,
181 pub current_connections: usize,
183 pub total_disconnections: u64,
185 pub total_pings: u64,
187 pub total_ping_failures: u64,
189}
190
191pub struct PeerConnectionTracker {
197 pub peers: HashMap<String, PeerConnectionInfo>,
199 pub event_log: VecDeque<ConnectionEvent>,
201 pub stats: TrackerStats,
203}
204
205impl PeerConnectionTracker {
206 pub fn new() -> Self {
208 Self {
209 peers: HashMap::new(),
210 event_log: VecDeque::with_capacity(MAX_EVENT_LOG + 1),
211 stats: TrackerStats::default(),
212 }
213 }
214
215 pub fn record_event(&mut self, event: ConnectionEvent) {
217 match &event {
221 ConnectionEvent::Connected {
222 peer_id,
223 address,
224 at_secs,
225 } => {
226 let entry = self
227 .peers
228 .entry(peer_id.clone())
229 .or_insert_with(|| PeerConnectionInfo::new(peer_id.clone(), address.clone()));
230 entry.state = ConnectionState::Connected;
231 entry.address = address.clone();
232 entry.connected_at_secs = Some(*at_secs);
233 entry.disconnected_at_secs = None;
234
235 self.stats.total_connections += 1;
236 self.stats.current_connections = self.count_connected();
237 }
238
239 ConnectionEvent::Disconnected {
240 peer_id, at_secs, ..
241 } => {
242 if let Some(entry) = self.peers.get_mut(peer_id) {
243 entry.state = ConnectionState::Disconnected;
244 entry.disconnected_at_secs = Some(*at_secs);
245 }
246 self.stats.total_disconnections += 1;
247 self.stats.current_connections = self.count_connected();
248 }
249
250 ConnectionEvent::ProtocolNegotiated {
251 peer_id, protocol, ..
252 } => {
253 if let Some(entry) = self.peers.get_mut(peer_id) {
254 if !entry.protocols.contains(protocol) {
255 entry.protocols.push(protocol.clone());
256 }
257 }
258 }
259
260 ConnectionEvent::PingSuccess {
261 peer_id, rtt_ms, ..
262 } => {
263 if let Some(entry) = self.peers.get_mut(peer_id) {
264 entry.ping_count += 1;
265 let n = entry.ping_count as f64;
266 entry.avg_rtt_ms = (entry.avg_rtt_ms * (n - 1.0) + rtt_ms) / n;
267 }
268 self.stats.total_pings += 1;
269 }
270
271 ConnectionEvent::PingFailed { peer_id, .. } => {
272 if let Some(entry) = self.peers.get_mut(peer_id) {
273 entry.ping_count += 1;
274 entry.ping_failures += 1;
275 }
276 self.stats.total_pings += 1;
277 self.stats.total_ping_failures += 1;
278 }
279
280 ConnectionEvent::AddressChanged {
281 peer_id,
282 new_address,
283 ..
284 } => {
285 if let Some(entry) = self.peers.get_mut(peer_id) {
286 entry.address = new_address.clone();
287 }
288 }
289 }
290
291 self.event_log.push_back(event);
295 while self.event_log.len() > MAX_EVENT_LOG {
296 self.event_log.pop_front();
297 }
298 }
299
300 pub fn connected_peers(&self) -> Vec<&PeerConnectionInfo> {
302 self.peers
303 .values()
304 .filter(|p| p.state == ConnectionState::Connected)
305 .collect()
306 }
307
308 pub fn peer_info(&self, peer_id: &str) -> Option<&PeerConnectionInfo> {
310 self.peers.get(peer_id)
311 }
312
313 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
317 let removed = self.peers.remove(peer_id).is_some();
318 if removed {
319 self.stats.current_connections = self.count_connected();
320 }
321 removed
322 }
323
324 pub fn unreliable_peers(&self, threshold: f64) -> Vec<&PeerConnectionInfo> {
326 self.peers
327 .values()
328 .filter(|p| p.reliability() < threshold)
329 .collect()
330 }
331
332 pub fn top_peers_by_uptime(&self, n: usize, now_secs: u64) -> Vec<&PeerConnectionInfo> {
334 let mut peers: Vec<&PeerConnectionInfo> = self.peers.values().collect();
335 peers.sort_by_key(|b| std::cmp::Reverse(b.uptime_secs(now_secs)));
336 peers.truncate(n);
337 peers
338 }
339
340 pub fn stats(&self) -> &TrackerStats {
342 &self.stats
343 }
344
345 pub fn recent_events(&self, count: usize) -> Vec<&ConnectionEvent> {
347 let len = self.event_log.len();
348 let skip = len.saturating_sub(count);
349 self.event_log.iter().skip(skip).collect()
350 }
351
352 fn count_connected(&self) -> usize {
357 self.peers
358 .values()
359 .filter(|p| p.state == ConnectionState::Connected)
360 .count()
361 }
362}
363
364impl Default for PeerConnectionTracker {
365 fn default() -> Self {
366 Self::new()
367 }
368}
369
370#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn connected_event(peer_id: &str, addr: &str, at: u64) -> ConnectionEvent {
383 ConnectionEvent::Connected {
384 peer_id: peer_id.to_string(),
385 address: addr.to_string(),
386 at_secs: at,
387 }
388 }
389
390 fn disconnected_event(peer_id: &str, reason: &str, at: u64) -> ConnectionEvent {
391 ConnectionEvent::Disconnected {
392 peer_id: peer_id.to_string(),
393 reason: reason.to_string(),
394 at_secs: at,
395 }
396 }
397
398 fn ping_success(peer_id: &str, rtt: f64, at: u64) -> ConnectionEvent {
399 ConnectionEvent::PingSuccess {
400 peer_id: peer_id.to_string(),
401 rtt_ms: rtt,
402 at_secs: at,
403 }
404 }
405
406 fn ping_failed(peer_id: &str, at: u64) -> ConnectionEvent {
407 ConnectionEvent::PingFailed {
408 peer_id: peer_id.to_string(),
409 at_secs: at,
410 }
411 }
412
413 #[test]
417 fn test_new_empty_state() {
418 let tracker = PeerConnectionTracker::new();
419 assert!(tracker.peers.is_empty());
420 assert!(tracker.event_log.is_empty());
421 assert_eq!(tracker.stats.total_connections, 0);
422 assert_eq!(tracker.stats.current_connections, 0);
423 }
424
425 #[test]
429 fn test_record_connected_updates_state() {
430 let mut tracker = PeerConnectionTracker::new();
431 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
432
433 let info = tracker.peer_info("peer-1").expect("peer should exist");
434 assert_eq!(info.state, ConnectionState::Connected);
435 assert_eq!(info.address, "/ip4/1.2.3.4/tcp/4001");
436 assert_eq!(info.connected_at_secs, Some(1000));
437 assert_eq!(tracker.stats.total_connections, 1);
438 assert_eq!(tracker.stats.current_connections, 1);
439 }
440
441 #[test]
445 fn test_record_disconnected_updates_state() {
446 let mut tracker = PeerConnectionTracker::new();
447 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
448 tracker.record_event(disconnected_event("peer-1", "timeout", 2000));
449
450 let info = tracker.peer_info("peer-1").expect("peer should exist");
451 assert_eq!(info.state, ConnectionState::Disconnected);
452 assert_eq!(info.disconnected_at_secs, Some(2000));
453 assert_eq!(tracker.stats.total_disconnections, 1);
454 assert_eq!(tracker.stats.current_connections, 0);
455 }
456
457 #[test]
461 fn test_protocol_negotiated_appends() {
462 let mut tracker = PeerConnectionTracker::new();
463 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
464 tracker.record_event(ConnectionEvent::ProtocolNegotiated {
465 peer_id: "peer-1".to_string(),
466 protocol: "/ipfs/kad/1.0.0".to_string(),
467 at_secs: 1001,
468 });
469 tracker.record_event(ConnectionEvent::ProtocolNegotiated {
470 peer_id: "peer-1".to_string(),
471 protocol: "/ipfs/bitswap/1.2.0".to_string(),
472 at_secs: 1002,
473 });
474 tracker.record_event(ConnectionEvent::ProtocolNegotiated {
476 peer_id: "peer-1".to_string(),
477 protocol: "/ipfs/kad/1.0.0".to_string(),
478 at_secs: 1003,
479 });
480
481 let info = tracker.peer_info("peer-1").expect("peer should exist");
482 assert_eq!(info.protocols.len(), 2);
483 assert!(info.protocols.contains(&"/ipfs/kad/1.0.0".to_string()));
484 assert!(info.protocols.contains(&"/ipfs/bitswap/1.2.0".to_string()));
485 }
486
487 #[test]
491 fn test_ping_success_avg_rtt() {
492 let mut tracker = PeerConnectionTracker::new();
493 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
494
495 tracker.record_event(ping_success("peer-1", 10.0, 1001));
496 tracker.record_event(ping_success("peer-1", 20.0, 1002));
497 tracker.record_event(ping_success("peer-1", 30.0, 1003));
498
499 let info = tracker.peer_info("peer-1").expect("peer should exist");
500 assert_eq!(info.ping_count, 3);
501 let expected = 20.0_f64;
503 assert!(
504 (info.avg_rtt_ms - expected).abs() < 1e-9,
505 "avg_rtt_ms = {} expected {}",
506 info.avg_rtt_ms,
507 expected
508 );
509 }
510
511 #[test]
515 fn test_ping_failed_increments_failures() {
516 let mut tracker = PeerConnectionTracker::new();
517 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
518 tracker.record_event(ping_failed("peer-1", 1001));
519 tracker.record_event(ping_failed("peer-1", 1002));
520
521 let info = tracker.peer_info("peer-1").expect("peer should exist");
522 assert_eq!(info.ping_count, 2);
523 assert_eq!(info.ping_failures, 2);
524 assert_eq!(tracker.stats.total_ping_failures, 2);
525 }
526
527 #[test]
531 fn test_address_changed_updates_address() {
532 let mut tracker = PeerConnectionTracker::new();
533 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
534 tracker.record_event(ConnectionEvent::AddressChanged {
535 peer_id: "peer-1".to_string(),
536 new_address: "/ip4/5.6.7.8/tcp/4001".to_string(),
537 at_secs: 1010,
538 });
539
540 let info = tracker.peer_info("peer-1").expect("peer should exist");
541 assert_eq!(info.address, "/ip4/5.6.7.8/tcp/4001");
542 }
543
544 #[test]
548 fn test_event_log_bounded_at_500() {
549 let mut tracker = PeerConnectionTracker::new();
550 for i in 0u64..600 {
552 tracker.record_event(ping_success("peer-1", i as f64, i));
553 }
554 assert_eq!(tracker.event_log.len(), 500);
555 }
556
557 #[test]
561 fn test_connected_peers_filters() {
562 let mut tracker = PeerConnectionTracker::new();
563 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
564 tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1000));
565 tracker.record_event(disconnected_event("peer-2", "bye", 2000));
566
567 let connected = tracker.connected_peers();
568 assert_eq!(connected.len(), 1);
569 assert_eq!(connected[0].peer_id, "peer-1");
570 }
571
572 #[test]
576 fn test_peer_info_some_none() {
577 let mut tracker = PeerConnectionTracker::new();
578 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
579
580 assert!(tracker.peer_info("peer-1").is_some());
581 assert!(tracker.peer_info("unknown").is_none());
582 }
583
584 #[test]
588 fn test_remove_peer_true_false() {
589 let mut tracker = PeerConnectionTracker::new();
590 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
591
592 assert!(tracker.remove_peer("peer-1"));
593 assert!(!tracker.remove_peer("peer-1")); assert!(!tracker.remove_peer("ghost")); }
596
597 #[test]
601 fn test_unreliable_peers_threshold() {
602 let mut tracker = PeerConnectionTracker::new();
603 tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 1000));
605 tracker.record_event(ping_success("peer-1", 10.0, 1001));
606 tracker.record_event(ping_failed("peer-1", 1002));
607
608 tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1000));
610 tracker.record_event(ping_success("peer-2", 10.0, 1001));
611 tracker.record_event(ping_success("peer-2", 10.0, 1002));
612
613 let unreliable = tracker.unreliable_peers(0.8);
614 assert_eq!(unreliable.len(), 1);
615 assert_eq!(unreliable[0].peer_id, "peer-1");
616 }
617
618 #[test]
622 fn test_top_peers_by_uptime_sorted() {
623 let mut tracker = PeerConnectionTracker::new();
624 tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 100));
626 tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 200));
627 tracker.record_event(connected_event("peer-3", "/ip4/3.3.3.3/tcp/4001", 300));
628
629 let now = 1000u64;
630 let top = tracker.top_peers_by_uptime(3, now);
631
632 assert_eq!(top.len(), 3);
634 assert_eq!(top[0].peer_id, "peer-1");
635 assert_eq!(top[1].peer_id, "peer-2");
636 assert_eq!(top[2].peer_id, "peer-3");
637 }
638
639 #[test]
643 fn test_top_peers_by_uptime_limit() {
644 let mut tracker = PeerConnectionTracker::new();
645 for i in 0u64..10 {
646 tracker.record_event(connected_event(
647 &format!("peer-{i}"),
648 "/ip4/1.1.1.1/tcp/4001",
649 i * 10,
650 ));
651 }
652 let top = tracker.top_peers_by_uptime(3, 1000);
653 assert_eq!(top.len(), 3);
654 }
655
656 #[test]
660 fn test_reliability_calculation() {
661 let mut tracker = PeerConnectionTracker::new();
662 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
663
664 let info = tracker.peer_info("peer-1").expect("peer should exist");
666 assert!((info.reliability() - 1.0).abs() < 1e-9);
667
668 tracker.record_event(ping_success("peer-1", 10.0, 1001));
670 tracker.record_event(ping_success("peer-1", 10.0, 1002));
671 tracker.record_event(ping_success("peer-1", 10.0, 1003));
672 tracker.record_event(ping_failed("peer-1", 1004));
673
674 let info = tracker.peer_info("peer-1").expect("peer should exist");
675 let expected = 1.0 - (1.0 / 4.0);
676 assert!(
677 (info.reliability() - expected).abs() < 1e-9,
678 "got {}",
679 info.reliability()
680 );
681 }
682
683 #[test]
687 fn test_uptime_secs_connected_and_not() {
688 let mut tracker = PeerConnectionTracker::new();
689 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 500));
690
691 let info = tracker.peer_info("peer-1").expect("peer should exist");
692 assert_eq!(info.uptime_secs(1000), 500);
693
694 tracker.record_event(disconnected_event("peer-1", "bye", 800));
695 let info = tracker.peer_info("peer-1").expect("peer should exist");
696 assert_eq!(info.uptime_secs(1000), 0);
697 }
698
699 #[test]
703 fn test_stats_totals_updated() {
704 let mut tracker = PeerConnectionTracker::new();
705 tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 1000));
706 tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1001));
707 tracker.record_event(ping_success("peer-1", 15.0, 1002));
708 tracker.record_event(ping_failed("peer-2", 1003));
709 tracker.record_event(disconnected_event("peer-1", "timeout", 2000));
710
711 let s = tracker.stats();
712 assert_eq!(s.total_connections, 2);
713 assert_eq!(s.current_connections, 1); assert_eq!(s.total_disconnections, 1);
715 assert_eq!(s.total_pings, 2);
716 assert_eq!(s.total_ping_failures, 1);
717 }
718
719 #[test]
723 fn test_recent_events_last_n() {
724 let mut tracker = PeerConnectionTracker::new();
725 tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
726 tracker.record_event(ping_success("peer-1", 10.0, 1001));
727 tracker.record_event(ping_success("peer-1", 20.0, 1002));
728 tracker.record_event(ping_failed("peer-1", 1003));
729 tracker.record_event(disconnected_event("peer-1", "done", 1004));
730
731 let recent = tracker.recent_events(3);
733 assert_eq!(recent.len(), 3);
734 assert!(matches!(recent[2], ConnectionEvent::Disconnected { .. }));
736 assert!(matches!(recent[1], ConnectionEvent::PingFailed { .. }));
738
739 let all = tracker.recent_events(100);
741 assert_eq!(all.len(), 5);
742 }
743}