haystack_server/session/
profile.rs1use std::time::Instant;
2
3use dashmap::DashMap;
4
5use crate::domain_scope::DomainScope;
6
7use super::affinity::ConnectorAffinity;
8use super::working_set::WorkingSetCache;
9
10pub 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 pub fn touch(&mut self) {
34 self.last_activity = Instant::now();
35 }
36
37 pub fn is_idle(&self, max_idle_secs: u64) -> bool {
39 self.last_activity.elapsed().as_secs() >= max_idle_secs
40 }
41}
42
43pub 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 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 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 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 pub fn remove(&self, session_id: u64) -> Option<(u64, SessionProfile)> {
79 self.profiles.remove(&session_id)
80 }
81
82 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 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 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 assert!(!profile.is_idle(3600));
139 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 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}