Skip to main content

haystack_server/session/
profile.rs

1use std::time::Instant;
2
3use dashmap::DashMap;
4
5use crate::domain_scope::DomainScope;
6
7use super::affinity::ConnectorAffinity;
8use super::working_set::WorkingSetCache;
9
10/// Per-session profile combining all session optimization state.
11pub struct SessionProfile {
12    pub user_id: String,
13    pub domain_scope: DomainScope,
14    pub affinity: ConnectorAffinity,
15    pub working_set: WorkingSetCache,
16    pub created_at: Instant,
17    pub last_activity: Instant,
18}
19
20impl SessionProfile {
21    pub fn new(user_id: String, domain_scope: DomainScope) -> Self {
22        Self {
23            user_id,
24            domain_scope,
25            affinity: ConnectorAffinity::new(),
26            working_set: WorkingSetCache::new(256),
27            created_at: Instant::now(),
28            last_activity: Instant::now(),
29        }
30    }
31
32    /// Update last activity timestamp.
33    pub fn touch(&mut self) {
34        self.last_activity = Instant::now();
35    }
36
37    /// Check if this session has been idle for longer than the given duration.
38    pub fn is_idle(&self, max_idle_secs: u64) -> bool {
39        self.last_activity.elapsed().as_secs() >= max_idle_secs
40    }
41}
42
43/// Registry of active sessions.
44pub struct SessionRegistry {
45    profiles: DashMap<u64, SessionProfile>,
46}
47
48impl SessionRegistry {
49    pub fn new() -> Self {
50        Self {
51            profiles: DashMap::new(),
52        }
53    }
54
55    /// Create a new session profile.
56    pub fn create(&self, session_id: u64, user_id: String, scope: DomainScope) {
57        self.profiles
58            .insert(session_id, SessionProfile::new(user_id, scope));
59    }
60
61    /// Get a session profile for reading.
62    pub fn get(
63        &self,
64        session_id: u64,
65    ) -> Option<dashmap::mapref::one::Ref<'_, u64, SessionProfile>> {
66        self.profiles.get(&session_id)
67    }
68
69    /// Get a mutable reference to a session profile.
70    pub fn get_mut(
71        &self,
72        session_id: u64,
73    ) -> Option<dashmap::mapref::one::RefMut<'_, u64, SessionProfile>> {
74        self.profiles.get_mut(&session_id)
75    }
76
77    /// Remove a session.
78    pub fn remove(&self, session_id: u64) -> Option<(u64, SessionProfile)> {
79        self.profiles.remove(&session_id)
80    }
81
82    /// Evict sessions idle for longer than max_idle_secs. Returns count evicted.
83    pub fn evict_idle(&self, max_idle_secs: u64) -> usize {
84        let to_remove: Vec<u64> = self
85            .profiles
86            .iter()
87            .filter(|entry| entry.value().is_idle(max_idle_secs))
88            .map(|entry| *entry.key())
89            .collect();
90        let count = to_remove.len();
91        for id in to_remove {
92            self.profiles.remove(&id);
93        }
94        count
95    }
96
97    /// Number of active sessions.
98    pub fn len(&self) -> usize {
99        self.profiles.len()
100    }
101
102    pub fn is_empty(&self) -> bool {
103        self.profiles.is_empty()
104    }
105}
106
107impl Default for SessionRegistry {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn session_profile_creation() {
119        let profile = SessionProfile::new("user1".into(), DomainScope::all());
120        assert_eq!(profile.user_id, "user1");
121        assert!(!profile.is_idle(3600));
122    }
123
124    #[test]
125    fn session_touch_updates_activity() {
126        let mut profile = SessionProfile::new("user1".into(), DomainScope::all());
127        let before = profile.last_activity;
128        // Small sleep to ensure time advances
129        std::thread::sleep(std::time::Duration::from_millis(10));
130        profile.touch();
131        assert!(profile.last_activity > before);
132    }
133
134    #[test]
135    fn session_idle_detection() {
136        let mut profile = SessionProfile::new("user1".into(), DomainScope::all());
137        // Not idle for a long threshold
138        assert!(!profile.is_idle(3600));
139        // Force last_activity into the past
140        profile.last_activity = Instant::now() - std::time::Duration::from_secs(100);
141        assert!(profile.is_idle(50));
142        assert!(!profile.is_idle(200));
143    }
144
145    #[test]
146    fn registry_create_get_remove() {
147        let reg = SessionRegistry::new();
148        assert!(reg.is_empty());
149
150        reg.create(1, "user1".into(), DomainScope::all());
151        assert_eq!(reg.len(), 1);
152
153        let profile = reg.get(1);
154        assert!(profile.is_some());
155        assert_eq!(profile.unwrap().user_id, "user1");
156
157        assert!(reg.get(999).is_none());
158
159        let removed = reg.remove(1);
160        assert!(removed.is_some());
161        assert!(reg.is_empty());
162    }
163
164    #[test]
165    fn registry_get_mut() {
166        let reg = SessionRegistry::new();
167        reg.create(1, "user1".into(), DomainScope::all());
168
169        if let Some(mut profile) = reg.get_mut(1) {
170            profile.touch();
171            profile.affinity.record_hit("conn_a");
172        }
173
174        let profile = reg.get(1).unwrap();
175        assert_eq!(profile.affinity.owner_of("missing"), None);
176    }
177
178    #[test]
179    fn registry_evict_idle() {
180        let reg = SessionRegistry::new();
181        reg.create(1, "active".into(), DomainScope::all());
182        reg.create(2, "idle".into(), DomainScope::all());
183
184        // Make session 2 idle
185        if let Some(mut profile) = reg.get_mut(2) {
186            profile.last_activity = Instant::now() - std::time::Duration::from_secs(100);
187        }
188
189        let evicted = reg.evict_idle(50);
190        assert_eq!(evicted, 1);
191        assert_eq!(reg.len(), 1);
192        assert!(reg.get(1).is_some());
193        assert!(reg.get(2).is_none());
194    }
195
196    #[test]
197    fn registry_default() {
198        let reg = SessionRegistry::default();
199        assert!(reg.is_empty());
200    }
201}