use std::collections::HashMap;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use uuid::Uuid;
const MAX_SESSIONS: usize = 10_000;
struct McpSession {
last_active: Instant,
}
pub struct SessionStore {
sessions: Mutex<HashMap<String, McpSession>>,
ttl: Duration,
}
impl SessionStore {
pub fn new(ttl: Duration) -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
ttl,
}
}
pub fn create(&self) -> Option<String> {
let mut sessions = self.sessions.lock();
if sessions.len() >= MAX_SESSIONS {
let now = Instant::now();
sessions.retain(|_, session| now.duration_since(session.last_active) < self.ttl);
if sessions.len() >= MAX_SESSIONS {
return None;
}
}
let id = Uuid::new_v4().to_string();
sessions.insert(
id.clone(),
McpSession {
last_active: Instant::now(),
},
);
Some(id)
}
pub fn touch(&self, id: &str) -> bool {
let mut sessions = self.sessions.lock();
if let Some(session) = sessions.get_mut(id) {
session.last_active = Instant::now();
true
} else {
false
}
}
pub fn remove(&self, id: &str) {
self.sessions.lock().remove(id);
}
pub fn evict_expired(&self) {
let now = Instant::now();
self.sessions
.lock()
.retain(|_, session| now.duration_since(session.last_active) < self.ttl);
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.sessions.lock().len()
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.sessions.lock().is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_and_touch() {
let store = SessionStore::new(Duration::from_secs(60));
let id = store.create().expect("under cap");
assert!(store.touch(&id));
assert!(!store.touch("nonexistent"));
assert_eq!(store.len(), 1);
}
#[test]
fn remove_session() {
let store = SessionStore::new(Duration::from_secs(60));
let id = store.create().expect("under cap");
assert_eq!(store.len(), 1);
store.remove(&id);
assert_eq!(store.len(), 0);
assert!(!store.touch(&id));
}
#[test]
fn evict_expired() {
let store = SessionStore::new(Duration::from_millis(1));
store.create().expect("under cap");
store.create().expect("under cap");
assert_eq!(store.len(), 2);
std::thread::sleep(Duration::from_millis(10));
store.evict_expired();
assert_eq!(store.len(), 0);
}
#[test]
fn create_fails_closed_at_cap_but_reclaims_expired() {
let store = SessionStore::new(Duration::from_millis(1));
{
let mut sessions = store.sessions.lock();
for _ in 0..MAX_SESSIONS {
sessions.insert(
Uuid::new_v4().to_string(),
McpSession {
last_active: Instant::now(),
},
);
}
}
std::thread::sleep(Duration::from_millis(10));
assert!(store.create().is_some());
let store = SessionStore::new(Duration::from_secs(3600));
{
let mut sessions = store.sessions.lock();
for _ in 0..MAX_SESSIONS {
sessions.insert(
Uuid::new_v4().to_string(),
McpSession {
last_active: Instant::now(),
},
);
}
}
assert!(store.create().is_none());
}
}