mongreldb_server/
sessions.rs1use std::collections::HashMap;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::Arc;
27use std::time::{Duration, Instant};
28
29use mongreldb_query::MongrelSession;
30
31pub struct SessionEntry {
33 pub session: Arc<MongrelSession>,
35 pub owner: String,
38 last_used: std::sync::Mutex<Instant>,
39 pub lock: tokio::sync::Mutex<()>,
41 closed: AtomicBool,
46}
47
48impl SessionEntry {
49 pub(crate) fn touch(&self) {
50 if let Ok(mut t) = self.last_used.lock() {
51 *t = Instant::now();
52 }
53 }
54
55 pub(crate) fn is_closed(&self) -> bool {
57 self.closed.load(Ordering::Acquire)
58 }
59
60 fn mark_closed(&self) {
61 self.closed.store(true, Ordering::Release);
62 }
63
64 fn idle_for_at_least(&self, timeout: Duration) -> bool {
65 self.last_used
66 .lock()
67 .map(|t| t.elapsed() >= timeout)
68 .unwrap_or(false)
69 }
70}
71
72pub struct SessionStore {
76 sessions: std::sync::Mutex<HashMap<String, Arc<SessionEntry>>>,
77 max_sessions: usize,
78 idle_timeout: Duration,
79}
80
81impl SessionStore {
82 pub fn new(max_sessions: usize, idle_timeout: Duration) -> Self {
85 Self {
86 sessions: std::sync::Mutex::new(HashMap::new()),
87 max_sessions: max_sessions.max(1),
90 idle_timeout,
91 }
92 }
93
94 pub fn create(&self, session: MongrelSession, owner: String) -> Option<String> {
97 let mut guard = self.sessions.lock().ok()?;
98 if guard.len() >= self.max_sessions {
99 return None;
100 }
101 let token = random_token()?;
102 guard.insert(
103 token.clone(),
104 Arc::new(SessionEntry {
105 session: Arc::new(session),
106 owner,
107 last_used: std::sync::Mutex::new(Instant::now()),
108 lock: tokio::sync::Mutex::new(()),
109 closed: AtomicBool::new(false),
110 }),
111 );
112 Some(token)
113 }
114
115 pub fn get(&self, token: &str, owner: &str) -> Option<Arc<SessionEntry>> {
122 let guard = self.sessions.lock().ok()?;
123 let entry = guard.get(token)?;
124 if entry.owner != owner || entry.is_closed() {
125 return None;
126 }
127 Some(Arc::clone(entry))
128 }
129
130 pub fn close(&self, token: &str, owner: &str) -> bool {
134 self.take_for_close(token, owner).is_some()
135 }
136
137 pub(crate) fn take_for_close(&self, token: &str, owner: &str) -> Option<Arc<SessionEntry>> {
140 if let Ok(mut guard) = self.sessions.lock() {
141 if let Some(entry) = guard.get(token) {
142 if entry.owner != owner {
143 return None;
144 }
145 entry.mark_closed();
146 }
147 return guard.remove(token);
148 }
149 None
150 }
151
152 pub fn sweep_idle(&self) -> usize {
156 let Ok(mut guard) = self.sessions.lock() else {
157 return 0;
158 };
159 let timeout = self.idle_timeout;
160 let to_evict: Vec<String> = guard
165 .iter()
166 .filter_map(|(token, entry)| {
167 if entry.idle_for_at_least(timeout)
168 && entry.session.query_registry().active_for_session(token) == 0
169 && entry.lock.try_lock().is_ok()
170 {
171 Some(token.clone())
172 } else {
173 None
174 }
175 })
176 .collect();
177 let count = to_evict.len();
178 for token in &to_evict {
179 if let Some(entry) = guard.remove(token) {
180 entry.mark_closed();
181 }
182 }
183 count
184 }
185
186 pub fn len(&self) -> usize {
188 self.sessions.lock().map(|g| g.len()).unwrap_or(0)
189 }
190
191 pub fn is_empty(&self) -> bool {
192 self.len() == 0
193 }
194
195 pub(crate) fn close_all(&self) {
197 if let Ok(mut guard) = self.sessions.lock() {
198 for entry in guard.values() {
199 entry.mark_closed();
200 }
201 guard.clear();
202 }
203 }
204}
205
206pub fn spawn_session_reaper(store: Arc<SessionStore>) {
210 std::thread::Builder::new()
211 .name("mongreldb-session-reaper".into())
212 .spawn(move || loop {
213 std::thread::sleep(Duration::from_secs(30));
214 let evicted = store.sweep_idle();
215 if evicted > 0 {
216 eprintln!("[session-reaper] evicted {evicted} idle session(s)");
217 }
218 })
219 .expect("spawn session-reaper");
220}
221
222fn random_token() -> Option<String> {
228 let bytes = read_urandom(16)?;
229 let mut n = 0u128;
230 for &b in &bytes {
231 n = (n << 8) | b as u128;
232 }
233 Some(format!("{n:032x}"))
234}
235
236fn read_urandom(n: usize) -> Option<Vec<u8>> {
237 use std::io::Read;
238 let mut f = std::fs::File::open("/dev/urandom").ok()?;
239 let mut buf = vec![0u8; n];
240 f.read_exact(&mut buf).ok()?;
241 Some(buf)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use mongreldb_core::Database;
248 use mongreldb_query::{RegisteredQueryGuard, SqlQueryOptions};
249 use tempfile::tempdir;
250
251 fn make_session() -> MongrelSession {
252 let dir = tempdir().unwrap();
253 let db = Arc::new(Database::create(dir.path()).unwrap());
254 std::mem::forget(dir);
257 MongrelSession::open(db).unwrap()
258 }
259
260 #[test]
261 fn create_and_get_roundtrip() {
262 let store = SessionStore::new(8, Duration::from_secs(60));
263 let token = store.create(make_session(), "alice".into()).unwrap();
264 assert!(store.get(&token, "alice").is_some());
265 assert!(store.get(&token, "eve").is_none());
267 assert_eq!(store.len(), 1);
268 }
269
270 #[test]
271 fn close_removes_session() {
272 let store = SessionStore::new(8, Duration::from_secs(60));
273 let token = store.create(make_session(), "alice".into()).unwrap();
274 assert!(store.close(&token, "alice"));
275 assert!(store.get(&token, "alice").is_none());
276 assert!(store.is_empty());
277 let t2 = store.create(make_session(), "bob".into()).unwrap();
279 assert!(!store.close(&t2, "alice"));
280 assert_eq!(store.len(), 1);
281 }
282
283 #[test]
284 fn capacity_limit_rejects_new_sessions() {
285 let store = SessionStore::new(1, Duration::from_secs(60));
286 assert!(store.create(make_session(), "a".into()).is_some());
287 assert!(store.create(make_session(), "b".into()).is_none());
289 assert_eq!(store.len(), 1);
290 }
291
292 #[test]
293 fn sweep_idle_evicts_stale_sessions() {
294 let store = SessionStore::new(8, Duration::from_millis(1));
295 let token = store.create(make_session(), "alice".into()).unwrap();
296 assert_eq!(store.len(), 1);
297 std::thread::sleep(Duration::from_millis(20));
299 let evicted = store.sweep_idle();
300 assert_eq!(evicted, 1);
301 assert!(store.get(&token, "alice").is_none());
302 assert!(store.is_empty());
303 }
304
305 #[test]
306 fn sweep_idle_keeps_active_queries() {
307 let store = SessionStore::new(8, Duration::from_millis(1));
308 let token = store.create(make_session(), "alice".into()).unwrap();
309 let entry = store.get(&token, "alice").unwrap();
310 let query = entry
311 .session
312 .register_query(SqlQueryOptions {
313 session_id: Some(token.clone()),
314 ..SqlQueryOptions::default()
315 })
316 .unwrap();
317 let query = RegisteredQueryGuard::new(query);
318 std::thread::sleep(Duration::from_millis(20));
319
320 assert_eq!(store.sweep_idle(), 0);
321 assert!(store.get(&token, "alice").is_some());
322
323 drop(query);
324 assert_eq!(store.sweep_idle(), 1);
325 assert!(store.is_empty());
326 }
327}