barbacane_lib/mcp/
session.rs1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3
4use parking_lot::Mutex;
5use uuid::Uuid;
6
7const MAX_SESSIONS: usize = 10_000;
13
14struct McpSession {
21 last_active: Instant,
22}
23
24pub struct SessionStore {
26 sessions: Mutex<HashMap<String, McpSession>>,
27 ttl: Duration,
28}
29
30impl SessionStore {
31 pub fn new(ttl: Duration) -> Self {
32 Self {
33 sessions: Mutex::new(HashMap::new()),
34 ttl,
35 }
36 }
37
38 pub fn create(&self) -> Option<String> {
41 let mut sessions = self.sessions.lock();
42 if sessions.len() >= MAX_SESSIONS {
43 let now = Instant::now();
45 sessions.retain(|_, session| now.duration_since(session.last_active) < self.ttl);
46 if sessions.len() >= MAX_SESSIONS {
47 return None;
48 }
49 }
50 let id = Uuid::new_v4().to_string();
51 sessions.insert(
52 id.clone(),
53 McpSession {
54 last_active: Instant::now(),
55 },
56 );
57 Some(id)
58 }
59
60 pub fn touch(&self, id: &str) -> bool {
62 let mut sessions = self.sessions.lock();
63 if let Some(session) = sessions.get_mut(id) {
64 session.last_active = Instant::now();
65 true
66 } else {
67 false
68 }
69 }
70
71 pub fn remove(&self, id: &str) {
73 self.sessions.lock().remove(id);
74 }
75
76 pub fn evict_expired(&self) {
78 let now = Instant::now();
79 self.sessions
80 .lock()
81 .retain(|_, session| now.duration_since(session.last_active) < self.ttl);
82 }
83
84 #[cfg(test)]
86 pub fn len(&self) -> usize {
87 self.sessions.lock().len()
88 }
89
90 #[cfg(test)]
92 pub fn is_empty(&self) -> bool {
93 self.sessions.lock().is_empty()
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn create_and_touch() {
103 let store = SessionStore::new(Duration::from_secs(60));
104 let id = store.create().expect("under cap");
105 assert!(store.touch(&id));
106 assert!(!store.touch("nonexistent"));
107 assert_eq!(store.len(), 1);
108 }
109
110 #[test]
111 fn remove_session() {
112 let store = SessionStore::new(Duration::from_secs(60));
113 let id = store.create().expect("under cap");
114 assert_eq!(store.len(), 1);
115 store.remove(&id);
116 assert_eq!(store.len(), 0);
117 assert!(!store.touch(&id));
118 }
119
120 #[test]
121 fn evict_expired() {
122 let store = SessionStore::new(Duration::from_millis(1));
123 store.create().expect("under cap");
124 store.create().expect("under cap");
125 assert_eq!(store.len(), 2);
126 std::thread::sleep(Duration::from_millis(10));
127 store.evict_expired();
128 assert_eq!(store.len(), 0);
129 }
130
131 #[test]
132 fn create_fails_closed_at_cap_but_reclaims_expired() {
133 let store = SessionStore::new(Duration::from_millis(1));
135 {
136 let mut sessions = store.sessions.lock();
137 for _ in 0..MAX_SESSIONS {
138 sessions.insert(
139 Uuid::new_v4().to_string(),
140 McpSession {
141 last_active: Instant::now(),
142 },
143 );
144 }
145 }
146 std::thread::sleep(Duration::from_millis(10));
147 assert!(store.create().is_some());
149
150 let store = SessionStore::new(Duration::from_secs(3600));
152 {
153 let mut sessions = store.sessions.lock();
154 for _ in 0..MAX_SESSIONS {
155 sessions.insert(
156 Uuid::new_v4().to_string(),
157 McpSession {
158 last_active: Instant::now(),
159 },
160 );
161 }
162 }
163 assert!(store.create().is_none());
164 }
165}