1use std::collections::HashMap;
44use std::time::{Duration, Instant};
45
46#[derive(Debug, Clone)]
48pub struct PeerLifetimeConfig {
49 pub disconnected_timeout: Duration,
52
53 pub connected_timeout: Duration,
57
58 pub cleanup_interval: Duration,
60}
61
62impl Default for PeerLifetimeConfig {
63 fn default() -> Self {
64 Self {
65 disconnected_timeout: Duration::from_secs(30),
66 connected_timeout: Duration::from_secs(60),
67 cleanup_interval: Duration::from_secs(10),
68 }
69 }
70}
71
72impl PeerLifetimeConfig {
73 pub fn new(
75 disconnected_timeout: Duration,
76 connected_timeout: Duration,
77 cleanup_interval: Duration,
78 ) -> Self {
79 Self {
80 disconnected_timeout,
81 connected_timeout,
82 cleanup_interval,
83 }
84 }
85
86 pub fn fast() -> Self {
88 Self {
89 disconnected_timeout: Duration::from_secs(5),
90 connected_timeout: Duration::from_secs(10),
91 cleanup_interval: Duration::from_secs(2),
92 }
93 }
94
95 pub fn relaxed() -> Self {
97 Self {
98 disconnected_timeout: Duration::from_secs(60),
99 connected_timeout: Duration::from_secs(120),
100 cleanup_interval: Duration::from_secs(30),
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107struct PeerState {
108 connected: bool,
110 last_seen: Instant,
112 first_seen: Instant,
114 disconnected_at: Option<Instant>,
116}
117
118impl PeerState {
119 fn new(connected: bool) -> Self {
120 let now = Instant::now();
121 Self {
122 connected,
123 last_seen: now,
124 first_seen: now,
125 disconnected_at: if connected { None } else { Some(now) },
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum StaleReason {
133 DisconnectedTimeout,
135 ConnectedTimeout,
137}
138
139#[derive(Debug, Clone)]
141pub struct StalePeerInfo {
142 pub address: String,
144 pub reason: StaleReason,
146 pub time_since_last_seen: Duration,
148 pub was_connected: bool,
150}
151
152#[derive(Debug)]
157pub struct PeerLifetimeManager {
158 config: PeerLifetimeConfig,
160 peers: HashMap<String, PeerState>,
162}
163
164impl PeerLifetimeManager {
165 pub fn new(config: PeerLifetimeConfig) -> Self {
167 Self {
168 config,
169 peers: HashMap::new(),
170 }
171 }
172
173 pub fn with_defaults() -> Self {
175 Self::new(PeerLifetimeConfig::default())
176 }
177
178 pub fn on_peer_activity(&mut self, address: &str, connected: bool) {
187 let now = Instant::now();
188
189 if let Some(state) = self.peers.get_mut(address) {
190 state.last_seen = now;
191 if connected && !state.connected {
192 state.connected = true;
194 state.disconnected_at = None;
195 log::debug!("Peer {} connected", address);
196 } else if !connected && state.connected {
197 state.connected = false;
199 state.disconnected_at = Some(now);
200 log::debug!("Peer {} disconnected", address);
201 }
202 } else {
203 log::debug!("New peer {} (connected: {})", address, connected);
205 self.peers
206 .insert(address.to_string(), PeerState::new(connected));
207 }
208 }
209
210 pub fn on_peer_disconnected(&mut self, address: &str) {
216 if let Some(state) = self.peers.get_mut(address) {
217 if state.connected {
218 state.connected = false;
219 state.disconnected_at = Some(Instant::now());
220 log::debug!("Peer {} marked as disconnected", address);
221 }
222 }
223 }
224
225 pub fn is_tracked(&self, address: &str) -> bool {
227 self.peers.contains_key(address)
228 }
229
230 pub fn is_connected(&self, address: &str) -> bool {
232 self.peers
233 .get(address)
234 .map(|s| s.connected)
235 .unwrap_or(false)
236 }
237
238 pub fn get_stale_peers(&self) -> Vec<StalePeerInfo> {
244 self.peers
245 .iter()
246 .filter_map(|(address, state)| {
247 let time_since_last_seen = state.last_seen.elapsed();
248
249 let (is_stale, reason) = if state.connected {
250 let is_stale = time_since_last_seen > self.config.connected_timeout;
252 (is_stale, StaleReason::ConnectedTimeout)
253 } else {
254 let is_stale = time_since_last_seen > self.config.disconnected_timeout;
256 (is_stale, StaleReason::DisconnectedTimeout)
257 };
258
259 if is_stale {
260 Some(StalePeerInfo {
261 address: address.clone(),
262 reason,
263 time_since_last_seen,
264 was_connected: state.connected,
265 })
266 } else {
267 None
268 }
269 })
270 .collect()
271 }
272
273 pub fn get_stale_peer_addresses(&self) -> Vec<String> {
275 self.get_stale_peers()
276 .into_iter()
277 .map(|info| info.address)
278 .collect()
279 }
280
281 pub fn remove_peer(&mut self, address: &str) -> bool {
285 if self.peers.remove(address).is_some() {
286 log::debug!("Removed peer {} from lifetime tracking", address);
287 true
288 } else {
289 false
290 }
291 }
292
293 pub fn cleanup_stale_peers(&mut self) -> Vec<StalePeerInfo> {
297 let stale = self.get_stale_peers();
298
299 for info in &stale {
300 self.peers.remove(&info.address);
301 }
302
303 if !stale.is_empty() {
304 log::debug!("Cleaned up {} stale peers", stale.len());
305 }
306
307 stale
308 }
309
310 pub fn stats(&self) -> PeerLifetimeStats {
312 let mut connected = 0;
313 let mut disconnected = 0;
314
315 for state in self.peers.values() {
316 if state.connected {
317 connected += 1;
318 } else {
319 disconnected += 1;
320 }
321 }
322
323 PeerLifetimeStats {
324 total_tracked: self.peers.len(),
325 connected,
326 disconnected,
327 }
328 }
329
330 pub fn get_peer_info(&self, address: &str) -> Option<PeerInfo> {
332 self.peers.get(address).map(|state| PeerInfo {
333 connected: state.connected,
334 time_since_last_seen: state.last_seen.elapsed(),
335 time_since_first_seen: state.first_seen.elapsed(),
336 time_since_disconnect: state.disconnected_at.map(|t| t.elapsed()),
337 })
338 }
339
340 pub fn clear(&mut self) {
342 let count = self.peers.len();
343 self.peers.clear();
344 if count > 0 {
345 log::debug!("Cleared {} peers from lifetime tracking", count);
346 }
347 }
348
349 pub fn tracked_count(&self) -> usize {
351 self.peers.len()
352 }
353
354 pub fn cleanup_interval(&self) -> Duration {
356 self.config.cleanup_interval
357 }
358}
359
360#[derive(Debug, Clone, Copy)]
362pub struct PeerLifetimeStats {
363 pub total_tracked: usize,
365 pub connected: usize,
367 pub disconnected: usize,
369}
370
371#[derive(Debug, Clone)]
373pub struct PeerInfo {
374 pub connected: bool,
376 pub time_since_last_seen: Duration,
378 pub time_since_first_seen: Duration,
380 pub time_since_disconnect: Option<Duration>,
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::thread::sleep;
388
389 #[test]
390 fn test_new_peer_tracking() {
391 let mut manager = PeerLifetimeManager::with_defaults();
392
393 assert!(!manager.is_tracked("test"));
394
395 manager.on_peer_activity("test", true);
396
397 assert!(manager.is_tracked("test"));
398 assert!(manager.is_connected("test"));
399 }
400
401 #[test]
402 fn test_peer_disconnect() {
403 let mut manager = PeerLifetimeManager::with_defaults();
404
405 manager.on_peer_activity("test", true);
406 assert!(manager.is_connected("test"));
407
408 manager.on_peer_disconnected("test");
409 assert!(!manager.is_connected("test"));
410 }
411
412 #[test]
413 fn test_stale_peer_detection() {
414 let config = PeerLifetimeConfig {
415 disconnected_timeout: Duration::from_millis(50),
416 connected_timeout: Duration::from_millis(100),
417 cleanup_interval: Duration::from_millis(10),
418 };
419 let mut manager = PeerLifetimeManager::new(config);
420
421 manager.on_peer_activity("test", false);
423
424 assert!(manager.get_stale_peers().is_empty());
426
427 sleep(Duration::from_millis(60));
429
430 let stale = manager.get_stale_peers();
432 assert_eq!(stale.len(), 1);
433 assert_eq!(stale[0].address, "test");
434 assert_eq!(stale[0].reason, StaleReason::DisconnectedTimeout);
435 }
436
437 #[test]
438 fn test_cleanup_stale_peers() {
439 let config = PeerLifetimeConfig {
440 disconnected_timeout: Duration::from_millis(10),
441 connected_timeout: Duration::from_millis(100),
442 cleanup_interval: Duration::from_millis(5),
443 };
444 let mut manager = PeerLifetimeManager::new(config);
445
446 manager.on_peer_activity("peer1", false);
447 manager.on_peer_activity("peer2", true);
448
449 sleep(Duration::from_millis(20));
450
451 let cleaned = manager.cleanup_stale_peers();
453 assert_eq!(cleaned.len(), 1);
454 assert_eq!(cleaned[0].address, "peer1");
455
456 assert!(!manager.is_tracked("peer1"));
458 assert!(manager.is_tracked("peer2"));
460 }
461
462 #[test]
463 fn test_stats() {
464 let mut manager = PeerLifetimeManager::with_defaults();
465
466 manager.on_peer_activity("connected1", true);
467 manager.on_peer_activity("connected2", true);
468 manager.on_peer_activity("disconnected1", false);
469
470 let stats = manager.stats();
471 assert_eq!(stats.total_tracked, 3);
472 assert_eq!(stats.connected, 2);
473 assert_eq!(stats.disconnected, 1);
474 }
475
476 #[test]
479 fn test_kotlin_timeout_values() {
480 let config = PeerLifetimeConfig::new(
482 Duration::from_secs(120),
483 Duration::from_secs(300),
484 Duration::from_secs(30),
485 );
486 assert_eq!(config.disconnected_timeout, Duration::from_secs(120));
487 assert_eq!(config.connected_timeout, Duration::from_secs(300));
488
489 let mut manager = PeerLifetimeManager::new(config);
490
491 manager.on_peer_activity("peer1", false);
493 assert!(manager.get_stale_peers().is_empty());
494
495 manager.on_peer_activity("peer2", true);
497 assert!(manager.get_stale_peers().is_empty());
498 }
499
500 #[test]
501 fn test_disconnect_does_not_update_last_seen() {
502 let config = PeerLifetimeConfig {
505 disconnected_timeout: Duration::from_millis(50),
506 connected_timeout: Duration::from_millis(200),
507 cleanup_interval: Duration::from_millis(10),
508 };
509 let mut manager = PeerLifetimeManager::new(config);
510
511 manager.on_peer_activity("test", true);
513
514 sleep(Duration::from_millis(30));
516
517 manager.on_peer_disconnected("test");
519
520 sleep(Duration::from_millis(30));
523
524 let stale = manager.get_stale_peers();
526 assert_eq!(stale.len(), 1);
527 assert_eq!(stale[0].address, "test");
528 }
529
530 #[test]
531 fn test_activity_resets_stale_timer() {
532 let config = PeerLifetimeConfig {
533 disconnected_timeout: Duration::from_millis(50),
534 connected_timeout: Duration::from_millis(100),
535 cleanup_interval: Duration::from_millis(10),
536 };
537 let mut manager = PeerLifetimeManager::new(config);
538
539 manager.on_peer_activity("test", false);
540
541 sleep(Duration::from_millis(40));
543
544 manager.on_peer_activity("test", false);
546
547 assert!(manager.get_stale_peers().is_empty());
549
550 sleep(Duration::from_millis(20));
552 assert!(manager.get_stale_peers().is_empty());
553
554 sleep(Duration::from_millis(40));
556 assert_eq!(manager.get_stale_peers().len(), 1);
557 }
558
559 #[test]
560 fn test_connected_peer_longer_timeout() {
561 let config = PeerLifetimeConfig {
563 disconnected_timeout: Duration::from_millis(30),
564 connected_timeout: Duration::from_millis(80),
565 cleanup_interval: Duration::from_millis(10),
566 };
567 let mut manager = PeerLifetimeManager::new(config);
568
569 manager.on_peer_activity("connected", true);
570 manager.on_peer_activity("disconnected", false);
571
572 sleep(Duration::from_millis(40));
574
575 let stale = manager.get_stale_peers();
576 assert_eq!(stale.len(), 1);
577 assert_eq!(stale[0].address, "disconnected");
578 assert_eq!(stale[0].reason, StaleReason::DisconnectedTimeout);
579
580 sleep(Duration::from_millis(50));
582
583 let stale = manager.get_stale_peers();
584 assert_eq!(stale.len(), 2);
585 }
586}