1use std::collections::HashMap;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum PeerSessionState {
41 Initiated,
43 Established,
45 Closing,
47 Closed,
49}
50
51impl PeerSessionState {
52 #[inline]
54 pub fn is_terminal(self) -> bool {
55 self == Self::Closed
56 }
57
58 #[inline]
62 pub fn is_active(self) -> bool {
63 !self.is_terminal()
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum SessionDirection {
74 Inbound,
76 Outbound,
78}
79
80#[derive(Clone, Debug)]
86pub struct PeerSessionEntry {
87 pub session_id: u64,
89 pub peer_id: String,
91 pub state: PeerSessionState,
93 pub direction: SessionDirection,
95 pub created_at_tick: u64,
97 pub last_active_tick: u64,
99 pub bytes_sent: u64,
101 pub bytes_received: u64,
103}
104
105impl PeerSessionEntry {
106 #[inline]
108 pub fn is_terminal(&self) -> bool {
109 self.state.is_terminal()
110 }
111
112 #[inline]
114 pub fn duration_ticks(&self, current_tick: u64) -> u64 {
115 current_tick.saturating_sub(self.created_at_tick)
116 }
117
118 #[inline]
120 pub fn idle_ticks(&self, current_tick: u64) -> u64 {
121 current_tick.saturating_sub(self.last_active_tick)
122 }
123}
124
125#[derive(Clone, Debug)]
131pub struct SessionManagerConfig {
132 pub max_sessions_per_peer: usize,
134 pub global_max_sessions: usize,
136 pub session_idle_timeout_ticks: u64,
138}
139
140impl Default for SessionManagerConfig {
141 fn default() -> Self {
142 Self {
143 max_sessions_per_peer: 8,
144 global_max_sessions: 256,
145 session_idle_timeout_ticks: 300,
146 }
147 }
148}
149
150#[derive(Clone, Debug, Default)]
156pub struct SessionManagerStats {
157 pub total_sessions: usize,
159 pub active_sessions: usize,
161 pub closed_sessions: usize,
163 pub by_state: HashMap<PeerSessionState, usize>,
165 pub total_bytes_sent: u64,
167 pub total_bytes_received: u64,
169}
170
171pub struct PeerSessionManager {
184 sessions: HashMap<u64, PeerSessionEntry>,
186 peer_sessions: HashMap<String, Vec<u64>>,
188 next_session_id: u64,
190 config: SessionManagerConfig,
192}
193
194impl PeerSessionManager {
195 pub fn new(config: SessionManagerConfig) -> Self {
197 Self {
198 sessions: HashMap::new(),
199 peer_sessions: HashMap::new(),
200 next_session_id: 1,
201 config,
202 }
203 }
204
205 fn global_active_count(&self) -> usize {
209 self.sessions
210 .values()
211 .filter(|s| s.state.is_active())
212 .count()
213 }
214
215 fn peer_active_count(&self, peer_id: &str) -> usize {
217 match self.peer_sessions.get(peer_id) {
218 None => 0,
219 Some(ids) => ids
220 .iter()
221 .filter_map(|id| self.sessions.get(id))
222 .filter(|s| s.state.is_active())
223 .count(),
224 }
225 }
226
227 pub fn open_session(
240 &mut self,
241 peer_id: &str,
242 direction: SessionDirection,
243 current_tick: u64,
244 ) -> Result<u64, String> {
245 if self.global_active_count() >= self.config.global_max_sessions {
247 return Err("global session limit reached".to_string());
248 }
249
250 if self.peer_active_count(peer_id) >= self.config.max_sessions_per_peer {
252 return Err("per-peer session limit reached".to_string());
253 }
254
255 let session_id = self.next_session_id;
256 self.next_session_id = self.next_session_id.wrapping_add(1);
257
258 let entry = PeerSessionEntry {
259 session_id,
260 peer_id: peer_id.to_string(),
261 state: PeerSessionState::Initiated,
262 direction,
263 created_at_tick: current_tick,
264 last_active_tick: current_tick,
265 bytes_sent: 0,
266 bytes_received: 0,
267 };
268
269 self.sessions.insert(session_id, entry);
270 self.peer_sessions
271 .entry(peer_id.to_string())
272 .or_default()
273 .push(session_id);
274
275 Ok(session_id)
276 }
277
278 pub fn transition(
286 &mut self,
287 session_id: u64,
288 new_state: PeerSessionState,
289 ) -> Result<(), String> {
290 let session = self
291 .sessions
292 .get_mut(&session_id)
293 .ok_or_else(|| "session not found".to_string())?;
294
295 if session.state == PeerSessionState::Closed {
296 return Err("invalid transition".to_string());
297 }
298
299 session.state = new_state;
300 Ok(())
301 }
302
303 pub fn record_activity(
310 &mut self,
311 session_id: u64,
312 bytes_sent: u64,
313 bytes_recv: u64,
314 current_tick: u64,
315 ) -> bool {
316 match self.sessions.get_mut(&session_id) {
317 None => false,
318 Some(session) => {
319 session.bytes_sent = session.bytes_sent.saturating_add(bytes_sent);
320 session.bytes_received = session.bytes_received.saturating_add(bytes_recv);
321 session.last_active_tick = current_tick;
322 true
323 }
324 }
325 }
326
327 pub fn evict_idle(&mut self, current_tick: u64) -> Vec<u64> {
333 let timeout = self.config.session_idle_timeout_ticks;
334 let mut evicted = Vec::new();
335
336 for session in self.sessions.values_mut() {
337 if session.state != PeerSessionState::Closed
338 && session.idle_ticks(current_tick) >= timeout
339 {
340 session.state = PeerSessionState::Closed;
341 evicted.push(session.session_id);
342 }
343 }
344
345 evicted
346 }
347
348 pub fn sessions_for_peer(&self, peer_id: &str) -> Vec<&PeerSessionEntry> {
350 match self.peer_sessions.get(peer_id) {
351 None => Vec::new(),
352 Some(ids) => ids.iter().filter_map(|id| self.sessions.get(id)).collect(),
353 }
354 }
355
356 pub fn active_count_for_peer(&self, peer_id: &str) -> usize {
358 self.peer_active_count(peer_id)
359 }
360
361 pub fn get_session(&self, session_id: u64) -> Option<&PeerSessionEntry> {
363 self.sessions.get(&session_id)
364 }
365
366 pub fn stats(&self) -> SessionManagerStats {
368 let mut by_state: HashMap<PeerSessionState, usize> = HashMap::new();
369 let mut total_bytes_sent: u64 = 0;
370 let mut total_bytes_received: u64 = 0;
371
372 for session in self.sessions.values() {
373 *by_state.entry(session.state).or_insert(0) += 1;
374 total_bytes_sent = total_bytes_sent.saturating_add(session.bytes_sent);
375 total_bytes_received = total_bytes_received.saturating_add(session.bytes_received);
376 }
377
378 let total_sessions = self.sessions.len();
379 let closed_sessions = by_state
380 .get(&PeerSessionState::Closed)
381 .copied()
382 .unwrap_or(0);
383 let active_sessions = total_sessions - closed_sessions;
384
385 SessionManagerStats {
386 total_sessions,
387 active_sessions,
388 closed_sessions,
389 by_state,
390 total_bytes_sent,
391 total_bytes_received,
392 }
393 }
394}
395
396pub type SessionState = PeerSessionState;
402
403pub type PeerSession = PeerSessionEntry;
405
406#[cfg(test)]
411mod tests {
412 use super::*;
413
414 fn default_mgr() -> PeerSessionManager {
415 PeerSessionManager::new(SessionManagerConfig::default())
416 }
417
418 #[test]
421 fn open_session_creates_session() {
422 let mut mgr = default_mgr();
423 let sid = mgr
424 .open_session("peer-a", SessionDirection::Inbound, 10)
425 .expect("open_session failed");
426 let session = mgr.get_session(sid).expect("session should exist");
427 assert_eq!(session.peer_id, "peer-a");
428 assert_eq!(session.state, PeerSessionState::Initiated);
429 assert_eq!(session.direction, SessionDirection::Inbound);
430 assert_eq!(session.created_at_tick, 10);
431 assert_eq!(session.last_active_tick, 10);
432 assert_eq!(session.bytes_sent, 0);
433 assert_eq!(session.bytes_received, 0);
434 }
435
436 #[test]
439 fn open_session_returns_unique_ids() {
440 let mut mgr = default_mgr();
441 let s1 = mgr
442 .open_session("peer-a", SessionDirection::Inbound, 0)
443 .expect("s1");
444 let s2 = mgr
445 .open_session("peer-a", SessionDirection::Outbound, 0)
446 .expect("s2");
447 assert_ne!(s1, s2);
448 }
449
450 #[test]
453 fn per_peer_cap_enforced() {
454 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
455 max_sessions_per_peer: 2,
456 global_max_sessions: 100,
457 ..Default::default()
458 });
459 mgr.open_session("peer-x", SessionDirection::Inbound, 0)
460 .expect("s1");
461 mgr.open_session("peer-x", SessionDirection::Outbound, 0)
462 .expect("s2");
463 let err = mgr
464 .open_session("peer-x", SessionDirection::Inbound, 0)
465 .expect_err("should be capped");
466 assert_eq!(err, "per-peer session limit reached");
467 }
468
469 #[test]
472 fn per_peer_cap_does_not_affect_other_peers() {
473 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
474 max_sessions_per_peer: 1,
475 global_max_sessions: 100,
476 ..Default::default()
477 });
478 mgr.open_session("peer-a", SessionDirection::Inbound, 0)
479 .expect("peer-a session");
480 mgr.open_session("peer-b", SessionDirection::Inbound, 0)
482 .expect("peer-b session");
483 }
484
485 #[test]
488 fn global_cap_enforced() {
489 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
490 max_sessions_per_peer: 100,
491 global_max_sessions: 2,
492 ..Default::default()
493 });
494 mgr.open_session("peer-a", SessionDirection::Inbound, 0)
495 .expect("s1");
496 mgr.open_session("peer-b", SessionDirection::Inbound, 0)
497 .expect("s2");
498 let err = mgr
499 .open_session("peer-c", SessionDirection::Inbound, 0)
500 .expect_err("should hit global cap");
501 assert_eq!(err, "global session limit reached");
502 }
503
504 #[test]
507 fn global_cap_freed_by_close() {
508 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
509 max_sessions_per_peer: 100,
510 global_max_sessions: 1,
511 ..Default::default()
512 });
513 let sid = mgr
514 .open_session("peer-a", SessionDirection::Inbound, 0)
515 .expect("s1");
516 assert!(mgr
518 .open_session("peer-b", SessionDirection::Inbound, 0)
519 .is_err());
520 mgr.transition(sid, PeerSessionState::Closed)
522 .expect("transition to Closed");
523 mgr.open_session("peer-b", SessionDirection::Inbound, 0)
525 .expect("s2 after slot freed");
526 }
527
528 #[test]
531 fn full_lifecycle_transition() {
532 let mut mgr = default_mgr();
533 let sid = mgr
534 .open_session("peer-a", SessionDirection::Outbound, 0)
535 .expect("open");
536
537 mgr.transition(sid, PeerSessionState::Established)
539 .expect("→ Established");
540 assert_eq!(
541 mgr.get_session(sid).expect("s").state,
542 PeerSessionState::Established
543 );
544
545 mgr.transition(sid, PeerSessionState::Closing)
547 .expect("→ Closing");
548 assert_eq!(
549 mgr.get_session(sid).expect("s").state,
550 PeerSessionState::Closing
551 );
552
553 mgr.transition(sid, PeerSessionState::Closed)
555 .expect("→ Closed");
556 assert_eq!(
557 mgr.get_session(sid).expect("s").state,
558 PeerSessionState::Closed
559 );
560 }
561
562 #[test]
565 fn transition_from_closed_is_invalid() {
566 let mut mgr = default_mgr();
567 let sid = mgr
568 .open_session("peer-a", SessionDirection::Inbound, 0)
569 .expect("open");
570 mgr.transition(sid, PeerSessionState::Closed)
571 .expect("close");
572 let err = mgr
573 .transition(sid, PeerSessionState::Established)
574 .expect_err("should be invalid");
575 assert_eq!(err, "invalid transition");
576 }
577
578 #[test]
581 fn transition_missing_session_returns_not_found() {
582 let mut mgr = default_mgr();
583 let err = mgr
584 .transition(9999, PeerSessionState::Established)
585 .expect_err("should not find session 9999");
586 assert_eq!(err, "session not found");
587 }
588
589 #[test]
592 fn record_activity_accumulates_and_updates_tick() {
593 let mut mgr = default_mgr();
594 let sid = mgr
595 .open_session("peer-a", SessionDirection::Inbound, 0)
596 .expect("open");
597
598 assert!(mgr.record_activity(sid, 100, 200, 5));
599 assert!(mgr.record_activity(sid, 50, 75, 10));
600
601 let session = mgr.get_session(sid).expect("session");
602 assert_eq!(session.bytes_sent, 150);
603 assert_eq!(session.bytes_received, 275);
604 assert_eq!(session.last_active_tick, 10);
605 }
606
607 #[test]
610 fn record_activity_returns_false_for_missing_session() {
611 let mut mgr = default_mgr();
612 assert!(!mgr.record_activity(0xdeadbeef, 1, 1, 0));
613 }
614
615 #[test]
618 fn evict_idle_marks_sessions_closed() {
619 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
620 session_idle_timeout_ticks: 10,
621 ..Default::default()
622 });
623 let s1 = mgr
624 .open_session("peer-a", SessionDirection::Inbound, 0)
625 .expect("s1");
626 let s2 = mgr
627 .open_session("peer-b", SessionDirection::Inbound, 5)
628 .expect("s2");
629
630 let evicted = mgr.evict_idle(15);
632 assert_eq!(evicted.len(), 2);
633 assert!(mgr.get_session(s1).expect("s1").state == PeerSessionState::Closed);
634 assert!(mgr.get_session(s2).expect("s2").state == PeerSessionState::Closed);
635 }
636
637 #[test]
640 fn evict_idle_spares_recently_active_sessions() {
641 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
642 session_idle_timeout_ticks: 100,
643 ..Default::default()
644 });
645 let s1 = mgr
646 .open_session("peer-a", SessionDirection::Inbound, 0)
647 .expect("s1");
648 mgr.record_activity(s1, 0, 0, 50);
650
651 let evicted = mgr.evict_idle(100);
653 assert!(evicted.is_empty());
654 assert_eq!(
655 mgr.get_session(s1).expect("s1").state,
656 PeerSessionState::Initiated
657 );
658 }
659
660 #[test]
663 fn evict_idle_skips_already_closed() {
664 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
665 session_idle_timeout_ticks: 1,
666 ..Default::default()
667 });
668 let sid = mgr
669 .open_session("peer-a", SessionDirection::Inbound, 0)
670 .expect("open");
671 mgr.transition(sid, PeerSessionState::Closed)
672 .expect("close");
673
674 let evicted = mgr.evict_idle(100);
676 assert!(evicted.is_empty());
677 }
678
679 #[test]
682 fn sessions_for_peer_returns_all_including_closed() {
683 let mut mgr = default_mgr();
684 let s1 = mgr
685 .open_session("peer-z", SessionDirection::Inbound, 0)
686 .expect("s1");
687 let s2 = mgr
688 .open_session("peer-z", SessionDirection::Outbound, 0)
689 .expect("s2");
690 mgr.transition(s1, PeerSessionState::Closed)
691 .expect("close s1");
692
693 let sessions = mgr.sessions_for_peer("peer-z");
694 assert_eq!(sessions.len(), 2);
695 let ids: Vec<u64> = sessions.iter().map(|s| s.session_id).collect();
696 assert!(ids.contains(&s1));
697 assert!(ids.contains(&s2));
698 }
699
700 #[test]
703 fn sessions_for_peer_returns_empty_for_unknown_peer() {
704 let mgr = default_mgr();
705 let sessions = mgr.sessions_for_peer("nobody");
706 assert!(sessions.is_empty());
707 }
708
709 #[test]
712 fn active_count_for_peer_excludes_closed() {
713 let mut mgr = default_mgr();
714 let s1 = mgr
715 .open_session("peer-q", SessionDirection::Inbound, 0)
716 .expect("s1");
717 mgr.open_session("peer-q", SessionDirection::Outbound, 0)
718 .expect("s2");
719 assert_eq!(mgr.active_count_for_peer("peer-q"), 2);
720
721 mgr.transition(s1, PeerSessionState::Closed).expect("close");
722 assert_eq!(mgr.active_count_for_peer("peer-q"), 1);
723 }
724
725 #[test]
728 fn active_count_for_peer_returns_zero_for_unknown() {
729 let mgr = default_mgr();
730 assert_eq!(mgr.active_count_for_peer("nobody"), 0);
731 }
732
733 #[test]
736 fn stats_by_state_counts_correctly() {
737 let mut mgr = default_mgr();
738 let s1 = mgr
739 .open_session("p1", SessionDirection::Inbound, 0)
740 .expect("s1");
741 let s2 = mgr
742 .open_session("p2", SessionDirection::Inbound, 0)
743 .expect("s2");
744 let s3 = mgr
745 .open_session("p3", SessionDirection::Outbound, 0)
746 .expect("s3");
747
748 mgr.transition(s1, PeerSessionState::Established)
749 .expect("s1 established");
750 mgr.transition(s2, PeerSessionState::Closing)
751 .expect("s2 closing");
752 mgr.transition(s3, PeerSessionState::Closed)
753 .expect("s3 closed");
754
755 let stats = mgr.stats();
756 assert_eq!(stats.total_sessions, 3);
757 assert_eq!(stats.active_sessions, 2); assert_eq!(stats.closed_sessions, 1); assert_eq!(
761 *stats
762 .by_state
763 .get(&PeerSessionState::Established)
764 .unwrap_or(&0),
765 1
766 );
767 assert_eq!(
768 *stats.by_state.get(&PeerSessionState::Closing).unwrap_or(&0),
769 1
770 );
771 assert_eq!(
772 *stats.by_state.get(&PeerSessionState::Closed).unwrap_or(&0),
773 1
774 );
775 }
776
777 #[test]
780 fn stats_aggregates_bytes() {
781 let mut mgr = default_mgr();
782 let s1 = mgr
783 .open_session("p1", SessionDirection::Inbound, 0)
784 .expect("s1");
785 let s2 = mgr
786 .open_session("p2", SessionDirection::Outbound, 0)
787 .expect("s2");
788
789 mgr.record_activity(s1, 1000, 2000, 1);
790 mgr.record_activity(s2, 500, 750, 1);
791
792 let stats = mgr.stats();
793 assert_eq!(stats.total_bytes_sent, 1500);
794 assert_eq!(stats.total_bytes_received, 2750);
795 }
796
797 #[test]
800 fn duration_ticks_helper() {
801 let mut mgr = default_mgr();
802 let sid = mgr
803 .open_session("peer-a", SessionDirection::Inbound, 100)
804 .expect("open");
805 let session = mgr.get_session(sid).expect("session");
806 assert_eq!(session.duration_ticks(150), 50);
807 assert_eq!(session.duration_ticks(100), 0);
808 assert_eq!(session.duration_ticks(50), 0);
810 }
811
812 #[test]
815 fn idle_ticks_helper() {
816 let mut mgr = default_mgr();
817 let sid = mgr
818 .open_session("peer-a", SessionDirection::Inbound, 0)
819 .expect("open");
820 mgr.record_activity(sid, 0, 0, 40);
821
822 let session = mgr.get_session(sid).expect("session");
823 assert_eq!(session.idle_ticks(60), 20);
824 assert_eq!(session.idle_ticks(40), 0);
825 }
826
827 #[test]
830 fn is_terminal_helper() {
831 let mut mgr = default_mgr();
832 let sid = mgr
833 .open_session("peer-a", SessionDirection::Inbound, 0)
834 .expect("open");
835 assert!(!mgr.get_session(sid).expect("s").is_terminal());
836 mgr.transition(sid, PeerSessionState::Closed)
837 .expect("close");
838 assert!(mgr.get_session(sid).expect("s").is_terminal());
839 }
840
841 #[test]
844 fn per_peer_cap_counts_only_active() {
845 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
846 max_sessions_per_peer: 1,
847 global_max_sessions: 100,
848 ..Default::default()
849 });
850 let s1 = mgr
851 .open_session("peer-a", SessionDirection::Inbound, 0)
852 .expect("s1");
853 assert!(mgr
855 .open_session("peer-a", SessionDirection::Inbound, 0)
856 .is_err());
857 mgr.transition(s1, PeerSessionState::Closed).expect("close");
859 mgr.open_session("peer-a", SessionDirection::Outbound, 0)
861 .expect("s2 after slot freed");
862 }
863
864 #[test]
867 fn evict_idle_returns_evicted_ids() {
868 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
869 session_idle_timeout_ticks: 5,
870 ..Default::default()
871 });
872 let s1 = mgr
873 .open_session("peer-a", SessionDirection::Inbound, 0)
874 .expect("s1");
875 let evicted = mgr.evict_idle(5);
877 assert_eq!(evicted, vec![s1]);
878 }
879
880 #[test]
883 fn get_session_returns_none_for_unknown() {
884 let mgr = default_mgr();
885 assert!(mgr.get_session(0xdeadbeef).is_none());
886 }
887
888 #[test]
891 fn outbound_direction_recorded() {
892 let mut mgr = default_mgr();
893 let sid = mgr
894 .open_session("peer-a", SessionDirection::Outbound, 0)
895 .expect("open");
896 assert_eq!(
897 mgr.get_session(sid).expect("s").direction,
898 SessionDirection::Outbound
899 );
900 }
901
902 #[test]
905 fn evict_idle_idempotent_for_closed() {
906 let mut mgr = PeerSessionManager::new(SessionManagerConfig {
907 session_idle_timeout_ticks: 1,
908 ..Default::default()
909 });
910 mgr.open_session("peer-a", SessionDirection::Inbound, 0)
911 .expect("open");
912
913 let first = mgr.evict_idle(10);
914 assert_eq!(first.len(), 1);
915
916 let second = mgr.evict_idle(20);
918 assert!(second.is_empty());
919 }
920
921 #[test]
924 fn stats_empty_manager() {
925 let mgr = default_mgr();
926 let stats = mgr.stats();
927 assert_eq!(stats.total_sessions, 0);
928 assert_eq!(stats.active_sessions, 0);
929 assert_eq!(stats.closed_sessions, 0);
930 assert_eq!(stats.total_bytes_sent, 0);
931 assert_eq!(stats.total_bytes_received, 0);
932 assert!(stats.by_state.is_empty());
933 }
934
935 #[test]
938 fn initiated_counted_as_active_in_stats() {
939 let mut mgr = default_mgr();
940 mgr.open_session("peer-a", SessionDirection::Inbound, 0)
941 .expect("open");
942 let stats = mgr.stats();
943 assert_eq!(stats.active_sessions, 1);
944 assert_eq!(stats.closed_sessions, 0);
945 assert_eq!(
946 *stats
947 .by_state
948 .get(&PeerSessionState::Initiated)
949 .unwrap_or(&0),
950 1
951 );
952 }
953}