use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use mongreldb_query::MongrelSession;
pub struct SessionEntry {
pub session: Arc<MongrelSession>,
pub owner: String,
last_used: std::sync::Mutex<Instant>,
pub lock: tokio::sync::Mutex<()>,
closed: AtomicBool,
}
impl SessionEntry {
pub(crate) fn touch(&self) {
if let Ok(mut t) = self.last_used.lock() {
*t = Instant::now();
}
}
pub(crate) fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
fn mark_closed(&self) {
self.closed.store(true, Ordering::Release);
}
fn idle_for_at_least(&self, timeout: Duration) -> bool {
self.last_used
.lock()
.map(|t| t.elapsed() >= timeout)
.unwrap_or(false)
}
}
pub struct SessionStore {
sessions: std::sync::Mutex<HashMap<String, Arc<SessionEntry>>>,
max_sessions: usize,
idle_timeout: Duration,
}
impl SessionStore {
pub fn new(max_sessions: usize, idle_timeout: Duration) -> Self {
Self {
sessions: std::sync::Mutex::new(HashMap::new()),
max_sessions: max_sessions.max(1),
idle_timeout,
}
}
pub fn create(&self, session: MongrelSession, owner: String) -> Option<String> {
let mut guard = self.sessions.lock().ok()?;
if guard.len() >= self.max_sessions {
return None;
}
let token = random_token()?;
guard.insert(
token.clone(),
Arc::new(SessionEntry {
session: Arc::new(session),
owner,
last_used: std::sync::Mutex::new(Instant::now()),
lock: tokio::sync::Mutex::new(()),
closed: AtomicBool::new(false),
}),
);
Some(token)
}
pub fn get(&self, token: &str, owner: &str) -> Option<Arc<SessionEntry>> {
let guard = self.sessions.lock().ok()?;
let entry = guard.get(token)?;
if entry.owner != owner || entry.is_closed() {
return None;
}
Some(Arc::clone(entry))
}
pub fn close(&self, token: &str, owner: &str) -> bool {
if let Ok(mut guard) = self.sessions.lock() {
if let Some(entry) = guard.get(token) {
if entry.owner != owner {
return false;
}
entry.mark_closed();
}
return guard.remove(token).is_some();
}
false
}
pub fn sweep_idle(&self) -> usize {
let Ok(mut guard) = self.sessions.lock() else {
return 0;
};
let timeout = self.idle_timeout;
let to_evict: Vec<String> = guard
.iter()
.filter_map(|(token, entry)| {
if entry.idle_for_at_least(timeout) && entry.lock.try_lock().is_ok() {
Some(token.clone())
} else {
None
}
})
.collect();
let count = to_evict.len();
for token in &to_evict {
if let Some(entry) = guard.remove(token) {
entry.mark_closed();
}
}
count
}
pub fn len(&self) -> usize {
self.sessions.lock().map(|g| g.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub fn spawn_session_reaper(store: Arc<SessionStore>) {
std::thread::Builder::new()
.name("mongreldb-session-reaper".into())
.spawn(move || loop {
std::thread::sleep(Duration::from_secs(30));
let evicted = store.sweep_idle();
if evicted > 0 {
eprintln!("[session-reaper] evicted {evicted} idle session(s)");
}
})
.expect("spawn session-reaper");
}
fn random_token() -> Option<String> {
let bytes = read_urandom(16)?;
let mut n = 0u128;
for &b in &bytes {
n = (n << 8) | b as u128;
}
Some(format!("{n:032x}"))
}
fn read_urandom(n: usize) -> Option<Vec<u8>> {
use std::io::Read;
let mut f = std::fs::File::open("/dev/urandom").ok()?;
let mut buf = vec![0u8; n];
f.read_exact(&mut buf).ok()?;
Some(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use mongreldb_core::Database;
use tempfile::tempdir;
fn make_session() -> MongrelSession {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
std::mem::forget(dir);
MongrelSession::open(db).unwrap()
}
#[test]
fn create_and_get_roundtrip() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
assert!(store.get(&token, "alice").is_some());
assert!(store.get(&token, "eve").is_none());
assert_eq!(store.len(), 1);
}
#[test]
fn close_removes_session() {
let store = SessionStore::new(8, Duration::from_secs(60));
let token = store.create(make_session(), "alice".into()).unwrap();
assert!(store.close(&token, "alice"));
assert!(store.get(&token, "alice").is_none());
assert!(store.is_empty());
let t2 = store.create(make_session(), "bob".into()).unwrap();
assert!(!store.close(&t2, "alice"));
assert_eq!(store.len(), 1);
}
#[test]
fn capacity_limit_rejects_new_sessions() {
let store = SessionStore::new(1, Duration::from_secs(60));
assert!(store.create(make_session(), "a".into()).is_some());
assert!(store.create(make_session(), "b".into()).is_none());
assert_eq!(store.len(), 1);
}
#[test]
fn sweep_idle_evicts_stale_sessions() {
let store = SessionStore::new(8, Duration::from_millis(1));
let token = store.create(make_session(), "alice".into()).unwrap();
assert_eq!(store.len(), 1);
std::thread::sleep(Duration::from_millis(20));
let evicted = store.sweep_idle();
assert_eq!(evicted, 1);
assert!(store.get(&token, "alice").is_none());
assert!(store.is_empty());
}
}