oxur_repl/server/
session.rs

1//! Session Management for REPL Server
2//!
3//! Manages multiple concurrent REPL sessions, each with its own evaluation context.
4//! Provides session lifecycle management: create, clone, lookup, close.
5//!
6//! Based on ODD-0018: Oxur Remote REPL Protocol Design
7
8use crate::eval::{EvalContext, EvalError};
9use crate::protocol::{ReplMode, SessionId};
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12use thiserror::Error;
13
14/// Session manager errors
15#[derive(Debug, Error)]
16pub enum SessionError {
17    #[error("Session not found: {0}")]
18    NotFound(String),
19
20    #[error("Session already exists: {0}")]
21    AlreadyExists(String),
22
23    #[error("Lock poisoned")]
24    LockPoisoned,
25
26    #[error("Evaluation failed")]
27    EvalFailed(#[from] EvalError),
28}
29
30pub type Result<T> = std::result::Result<T, SessionError>;
31
32/// Session information
33#[derive(Debug, Clone)]
34pub struct SessionInfo {
35    /// Session ID
36    pub id: SessionId,
37
38    /// Optional session name
39    pub name: Option<String>,
40
41    /// Evaluation mode (Lisp or Sexpr)
42    pub mode: ReplMode,
43
44    /// Number of evaluations performed
45    pub eval_count: u64,
46
47    /// Creation timestamp (milliseconds since epoch)
48    pub created_at: u64,
49
50    /// Last active timestamp (milliseconds since epoch)
51    pub last_active_at: u64,
52
53    /// Session timeout in milliseconds (default: 1 hour)
54    pub timeout_ms: u64,
55}
56
57/// Session metadata tracked separately from EvalContext
58#[derive(Debug, Clone)]
59struct SessionMetadata {
60    /// Optional session name
61    name: Option<String>,
62    /// Creation timestamp (milliseconds since epoch)
63    created_at: u64,
64    /// Last active timestamp (milliseconds since epoch)
65    last_active_at: u64,
66    /// Session timeout in milliseconds
67    timeout_ms: u64,
68}
69
70impl Default for SessionMetadata {
71    fn default() -> Self {
72        Self {
73            name: None,
74            created_at: std::time::SystemTime::now()
75                .duration_since(std::time::UNIX_EPOCH)
76                .unwrap()
77                .as_millis() as u64,
78            last_active_at: std::time::SystemTime::now()
79                .duration_since(std::time::UNIX_EPOCH)
80                .unwrap()
81                .as_millis() as u64,
82            timeout_ms: 3_600_000, // Default: 1 hour
83        }
84    }
85}
86
87/// Manages multiple REPL sessions
88///
89/// Thread-safe session storage using Arc<RwLock<HashMap>>.
90/// Each session has its own EvalContext with isolated state.
91#[derive(Debug, Clone)]
92pub struct SessionManager {
93    /// Active sessions (session_id -> context)
94    sessions: Arc<RwLock<HashMap<SessionId, EvalContext>>>,
95    /// Session metadata (session_id -> metadata)
96    metadata: Arc<RwLock<HashMap<SessionId, SessionMetadata>>>,
97}
98
99impl SessionManager {
100    /// Create a new session manager
101    pub fn new() -> Self {
102        Self {
103            sessions: Arc::new(RwLock::new(HashMap::new())),
104            metadata: Arc::new(RwLock::new(HashMap::new())),
105        }
106    }
107
108    /// Create a new session
109    ///
110    /// # Errors
111    ///
112    /// Returns `SessionError::AlreadyExists` if a session with the same ID already exists.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use oxur_repl::server::SessionManager;
118    /// use oxur_repl::protocol::{ReplMode, SessionId};
119    ///
120    /// let manager = SessionManager::new();
121    /// let session_id = manager.create(SessionId::new("session-1"), ReplMode::Lisp).unwrap();
122    /// assert_eq!(session_id, SessionId::new("session-1"));
123    /// ```
124    pub fn create(&self, session_id: SessionId, mode: ReplMode) -> Result<SessionId> {
125        let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
126
127        if sessions.contains_key(&session_id) {
128            return Err(SessionError::AlreadyExists(session_id.to_string()));
129        }
130
131        // Use EvalContext::new() for basic session management
132        // For full compilation support, use create_with_compilation()
133        let context = EvalContext::new(session_id.clone(), mode);
134        sessions.insert(session_id.clone(), context);
135
136        // Insert default metadata
137        let mut metadata = self.metadata.write().map_err(|_| SessionError::LockPoisoned)?;
138        metadata.insert(session_id.clone(), SessionMetadata::default());
139
140        Ok(session_id)
141    }
142
143    /// Create a new session with full compilation support
144    ///
145    /// This creates a session with the full compilation pipeline including
146    /// session directory, artifact cache, and subprocess executor.
147    ///
148    /// # Errors
149    ///
150    /// Returns `SessionError::AlreadyExists` if a session with the same ID already exists.
151    /// Returns `SessionError::EvalFailed` if the compilation pipeline cannot be initialized.
152    pub fn create_with_compilation(
153        &self,
154        session_id: SessionId,
155        mode: ReplMode,
156    ) -> Result<SessionId> {
157        let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
158
159        if sessions.contains_key(&session_id) {
160            return Err(SessionError::AlreadyExists(session_id.to_string()));
161        }
162
163        // Use with_compilation to get full session directory and artifact cache support
164        let context = EvalContext::with_compilation(session_id.clone(), mode)?;
165        sessions.insert(session_id.clone(), context);
166
167        // Insert default metadata
168        let mut metadata = self.metadata.write().map_err(|_| SessionError::LockPoisoned)?;
169        metadata.insert(session_id.clone(), SessionMetadata::default());
170
171        Ok(session_id)
172    }
173
174    /// Execute an async operation with a session
175    ///
176    /// Takes ownership of the evaluation, executes it, and updates the session.
177    /// This avoids lifetime issues with async closures.
178    ///
179    /// # Errors
180    ///
181    /// Returns `SessionError::NotFound` if the session doesn't exist.
182    /// Returns `SessionError::EvalFailed` if evaluation fails due to:
183    /// - Syntax errors in the code
184    /// - Type errors during compilation
185    /// - Runtime errors during execution
186    /// - Compilation errors
187    /// - Unsupported operations for the current tier
188    ///
189    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
190    pub async fn eval(
191        &self,
192        session_id: &SessionId,
193        code: impl AsRef<str>,
194    ) -> Result<crate::eval::EvalResult> {
195        // First, get a clone of the context to evaluate with
196        let mut context = {
197            let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
198
199            sessions
200                .get(session_id)
201                .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?
202                .clone()
203        };
204
205        // Evaluate the code
206        let result = context.eval(code.as_ref()).await?;
207
208        // Update the session with the new state
209        {
210            let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
211
212            sessions.insert(session_id.clone(), context.clone());
213        }
214
215        // Update last_active_at timestamp
216        {
217            let mut metadata = self.metadata.write().map_err(|_| SessionError::LockPoisoned)?;
218            if let Some(meta) = metadata.get_mut(session_id) {
219                meta.last_active_at = std::time::SystemTime::now()
220                    .duration_since(std::time::UNIX_EPOCH)
221                    .unwrap()
222                    .as_millis() as u64;
223            }
224        }
225
226        Ok(result)
227    }
228
229    /// Clone an existing session
230    ///
231    /// Creates a new session with the same cache and state as the source session,
232    /// but with a different session ID and reset statistics.
233    ///
234    /// # Errors
235    ///
236    /// Returns `SessionError::NotFound` if the source session doesn't exist.
237    /// Returns `SessionError::AlreadyExists` if the target session ID is already in use.
238    pub fn clone_session(&self, source_id: &SessionId, target_id: SessionId) -> Result<SessionId> {
239        let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
240
241        let source_context =
242            sessions.get(source_id).ok_or_else(|| SessionError::NotFound(source_id.to_string()))?;
243
244        if sessions.contains_key(&target_id) {
245            return Err(SessionError::AlreadyExists(target_id.to_string()));
246        }
247
248        let cloned_context = source_context.clone_to(target_id.clone());
249        sessions.insert(target_id.clone(), cloned_context);
250
251        Ok(target_id)
252    }
253
254    /// Close a session
255    ///
256    /// Removes the session and frees its resources.
257    ///
258    /// # Errors
259    ///
260    /// Returns `SessionError::NotFound` if the session doesn't exist.
261    pub fn close(&self, session_id: &SessionId) -> Result<()> {
262        let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
263
264        sessions
265            .remove(session_id)
266            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
267
268        Ok(())
269    }
270
271    /// List all active sessions
272    ///
273    /// Returns information about all currently active sessions.
274    ///
275    /// # Errors
276    ///
277    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
278    pub fn list(&self) -> Result<Vec<SessionInfo>> {
279        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
280        let metadata = self.metadata.read().map_err(|_| SessionError::LockPoisoned)?;
281
282        let mut infos: Vec<SessionInfo> = sessions
283            .iter()
284            .map(|(id, ctx)| {
285                let (tier1, tier2, _) = ctx.stats();
286                let meta = metadata.get(id).cloned().unwrap_or_default();
287
288                SessionInfo {
289                    id: id.clone(),
290                    name: meta.name,
291                    mode: ctx.mode(),
292                    eval_count: tier1 + tier2,
293                    created_at: meta.created_at,
294                    last_active_at: meta.last_active_at,
295                    timeout_ms: meta.timeout_ms,
296                }
297            })
298            .collect();
299
300        // Sort by session ID for consistent ordering
301        infos.sort_by(|a, b| a.id.cmp(&b.id));
302
303        Ok(infos)
304    }
305
306    /// Get information about a specific session
307    ///
308    /// # Errors
309    ///
310    /// Returns `SessionError::NotFound` if the session doesn't exist.
311    pub fn get_info(&self, session_id: &SessionId) -> Result<SessionInfo> {
312        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
313        let metadata = self.metadata.read().map_err(|_| SessionError::LockPoisoned)?;
314
315        let ctx = sessions
316            .get(session_id)
317            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
318
319        let (tier1, tier2, _) = ctx.stats();
320        let meta = metadata.get(session_id).cloned().unwrap_or_default();
321
322        Ok(SessionInfo {
323            id: session_id.clone(),
324            name: meta.name,
325            mode: ctx.mode(),
326            eval_count: tier1 + tier2,
327            created_at: meta.created_at,
328            last_active_at: meta.last_active_at,
329            timeout_ms: meta.timeout_ms,
330        })
331    }
332
333    /// Get the statistics collector for a session
334    ///
335    /// Returns a shared reference to the session's StatsCollector for detailed metrics access.
336    ///
337    /// # Errors
338    ///
339    /// Returns `SessionError::NotFound` if the session doesn't exist.
340    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
341    pub fn get_stats_collector(
342        &self,
343        session_id: &SessionId,
344    ) -> Result<Arc<std::sync::Mutex<crate::eval::EvalMetrics>>> {
345        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
346
347        let ctx = sessions
348            .get(session_id)
349            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
350
351        Ok(ctx.stats_collector())
352    }
353
354    /// Get the usage metrics collector for a session
355    ///
356    /// Returns a shared reference to the session's UsageMetrics for command frequency tracking.
357    ///
358    /// # Errors
359    ///
360    /// Returns `SessionError::NotFound` if the session doesn't exist.
361    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
362    pub fn get_usage_metrics(
363        &self,
364        session_id: &SessionId,
365    ) -> Result<Arc<std::sync::Mutex<crate::metrics::UsageMetrics>>> {
366        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
367
368        let ctx = sessions
369            .get(session_id)
370            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
371
372        Ok(ctx.usage_metrics())
373    }
374
375    /// Get resource statistics for a session
376    ///
377    /// Returns (session_dir_stats, artifact_cache_stats) for the given session.
378    ///
379    /// # Errors
380    ///
381    /// Returns `SessionError::NotFound` if the session doesn't exist.
382    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
383    pub fn get_resource_stats(
384        &self,
385        session_id: &SessionId,
386    ) -> Result<(Option<crate::session::DirStats>, Option<crate::cache::CacheStats>)> {
387        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
388
389        let ctx = sessions
390            .get(session_id)
391            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
392
393        Ok((ctx.session_dir_stats(), ctx.artifact_cache_stats()))
394    }
395
396    /// Get subprocess statistics for a session
397    ///
398    /// Returns subprocess metrics snapshot for the given session.
399    ///
400    /// # Errors
401    ///
402    /// Returns `SessionError::NotFound` if the session doesn't exist.
403    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
404    pub fn get_subprocess_stats(
405        &self,
406        session_id: &SessionId,
407    ) -> Result<Option<crate::metrics::SubprocessMetricsSnapshot>> {
408        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
409
410        let ctx = sessions
411            .get(session_id)
412            .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
413
414        Ok(ctx.subprocess_stats())
415    }
416
417    /// Get the number of active sessions
418    ///
419    /// # Errors
420    ///
421    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
422    pub fn count(&self) -> Result<usize> {
423        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
424
425        Ok(sessions.len())
426    }
427
428    /// Check if a session exists
429    ///
430    /// # Errors
431    ///
432    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
433    pub fn exists(&self, session_id: &SessionId) -> Result<bool> {
434        let sessions = self.sessions.read().map_err(|_| SessionError::LockPoisoned)?;
435
436        Ok(sessions.contains_key(session_id))
437    }
438
439    /// Close all sessions
440    ///
441    /// Useful for server shutdown. Returns the number of sessions that were closed.
442    ///
443    /// # Errors
444    ///
445    /// Returns `SessionError::LockPoisoned` if the internal lock is poisoned.
446    pub fn close_all(&self) -> Result<usize> {
447        let mut sessions = self.sessions.write().map_err(|_| SessionError::LockPoisoned)?;
448
449        let count = sessions.len();
450        sessions.clear();
451
452        Ok(count)
453    }
454}
455
456impl Default for SessionManager {
457    fn default() -> Self {
458        Self::new()
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_create_session() {
468        let manager = SessionManager::new();
469        let id = manager.create(SessionId::new("test-1"), ReplMode::Lisp).unwrap();
470
471        assert_eq!(id, SessionId::new("test-1"));
472        assert!(manager.exists(&id).unwrap());
473    }
474
475    #[test]
476    fn test_create_duplicate_session() {
477        let manager = SessionManager::new();
478        manager.create(SessionId::new("test-1"), ReplMode::Lisp).unwrap();
479
480        let result = manager.create(SessionId::new("test-1"), ReplMode::Lisp);
481        assert!(matches!(result, Err(SessionError::AlreadyExists(_))));
482    }
483
484    #[tokio::test]
485    async fn test_eval() {
486        let manager = SessionManager::new();
487        manager.create(SessionId::new("test"), ReplMode::Lisp).unwrap();
488
489        let result = manager.eval(&SessionId::new("test"), "(+ 1 2)").await.unwrap();
490
491        assert_eq!(result.value, "3");
492    }
493
494    #[tokio::test]
495    async fn test_eval_not_found() {
496        let manager = SessionManager::new();
497
498        let result = manager.eval(&SessionId::new("nonexistent"), "test").await;
499
500        assert!(matches!(result, Err(SessionError::NotFound(_))));
501    }
502
503    #[test]
504    fn test_clone_session() {
505        let manager = SessionManager::new();
506        manager.create(SessionId::new("source"), ReplMode::Lisp).unwrap();
507
508        let cloned_id =
509            manager.clone_session(&SessionId::new("source"), SessionId::new("target")).unwrap();
510
511        assert_eq!(cloned_id, SessionId::new("target"));
512        assert!(manager.exists(&SessionId::new("target")).unwrap());
513    }
514
515    #[test]
516    fn test_clone_nonexistent_session() {
517        let manager = SessionManager::new();
518
519        let result =
520            manager.clone_session(&SessionId::new("nonexistent"), SessionId::new("target"));
521
522        assert!(matches!(result, Err(SessionError::NotFound(_))));
523    }
524
525    #[test]
526    fn test_clone_to_existing_session() {
527        let manager = SessionManager::new();
528        manager.create(SessionId::new("source"), ReplMode::Lisp).unwrap();
529        manager.create(SessionId::new("target"), ReplMode::Lisp).unwrap();
530
531        let result = manager.clone_session(&SessionId::new("source"), SessionId::new("target"));
532
533        assert!(matches!(result, Err(SessionError::AlreadyExists(_))));
534    }
535
536    #[test]
537    fn test_close_session() {
538        let manager = SessionManager::new();
539        manager.create(SessionId::new("test"), ReplMode::Lisp).unwrap();
540
541        assert!(manager.exists(&SessionId::new("test")).unwrap());
542
543        manager.close(&SessionId::new("test")).unwrap();
544
545        assert!(!manager.exists(&SessionId::new("test")).unwrap());
546    }
547
548    #[test]
549    fn test_close_nonexistent_session() {
550        let manager = SessionManager::new();
551
552        let result = manager.close(&SessionId::new("nonexistent"));
553
554        assert!(matches!(result, Err(SessionError::NotFound(_))));
555    }
556
557    #[test]
558    fn test_list_sessions() {
559        let manager = SessionManager::new();
560        manager.create(SessionId::new("session-1"), ReplMode::Lisp).unwrap();
561        manager.create(SessionId::new("session-2"), ReplMode::Sexpr).unwrap();
562
563        let sessions = manager.list().unwrap();
564
565        assert_eq!(sessions.len(), 2);
566        assert_eq!(sessions[0].id, SessionId::new("session-1"));
567        assert_eq!(sessions[0].mode, ReplMode::Lisp);
568        assert_eq!(sessions[1].id, SessionId::new("session-2"));
569        assert_eq!(sessions[1].mode, ReplMode::Sexpr);
570    }
571
572    #[test]
573    fn test_list_empty() {
574        let manager = SessionManager::new();
575        let sessions = manager.list().unwrap();
576
577        assert_eq!(sessions.len(), 0);
578    }
579
580    #[test]
581    fn test_get_info() {
582        let manager = SessionManager::new();
583        manager.create(SessionId::new("test"), ReplMode::Lisp).unwrap();
584
585        let info = manager.get_info(&SessionId::new("test")).unwrap();
586
587        assert_eq!(info.id, SessionId::new("test"));
588        assert_eq!(info.mode, ReplMode::Lisp);
589        assert_eq!(info.eval_count, 0);
590    }
591
592    #[test]
593    fn test_get_info_nonexistent() {
594        let manager = SessionManager::new();
595
596        let result = manager.get_info(&SessionId::new("nonexistent"));
597
598        assert!(matches!(result, Err(SessionError::NotFound(_))));
599    }
600
601    #[tokio::test]
602    async fn test_eval_count_tracking() {
603        let manager = SessionManager::new();
604        manager.create(SessionId::new("test"), ReplMode::Lisp).unwrap();
605
606        // Perform some evaluations
607        manager.eval(&SessionId::new("test"), "(+ 1 2)").await.unwrap();
608
609        manager.eval(&SessionId::new("test"), "(* 3 4)").await.unwrap();
610
611        let info = manager.get_info(&SessionId::new("test")).unwrap();
612        assert_eq!(info.eval_count, 2);
613    }
614
615    #[test]
616    fn test_count() {
617        let manager = SessionManager::new();
618
619        assert_eq!(manager.count().unwrap(), 0);
620
621        manager.create(SessionId::new("session-1"), ReplMode::Lisp).unwrap();
622        assert_eq!(manager.count().unwrap(), 1);
623
624        manager.create(SessionId::new("session-2"), ReplMode::Sexpr).unwrap();
625        assert_eq!(manager.count().unwrap(), 2);
626
627        manager.close(&SessionId::new("session-1")).unwrap();
628        assert_eq!(manager.count().unwrap(), 1);
629    }
630
631    #[test]
632    fn test_close_all() {
633        let manager = SessionManager::new();
634        manager.create(SessionId::new("session-1"), ReplMode::Lisp).unwrap();
635        manager.create(SessionId::new("session-2"), ReplMode::Sexpr).unwrap();
636        manager.create(SessionId::new("session-3"), ReplMode::Lisp).unwrap();
637
638        assert_eq!(manager.count().unwrap(), 3);
639
640        let closed = manager.close_all().unwrap();
641
642        assert_eq!(closed, 3);
643        assert_eq!(manager.count().unwrap(), 0);
644    }
645
646    #[test]
647    fn test_thread_safety() {
648        use std::sync::Arc;
649        use std::thread;
650
651        let manager = Arc::new(SessionManager::new());
652        let mut handles = vec![];
653
654        // Create sessions from multiple threads
655        for i in 0..10 {
656            let manager_clone = Arc::clone(&manager);
657            let handle = thread::spawn(move || {
658                manager_clone
659                    .create(SessionId::new(format!("session-{}", i)), ReplMode::Lisp)
660                    .unwrap();
661            });
662            handles.push(handle);
663        }
664
665        for handle in handles {
666            handle.join().unwrap();
667        }
668
669        assert_eq!(manager.count().unwrap(), 10);
670    }
671}