nexo_core/session/
manager.rs1use std::sync::{Arc, Mutex};
2use std::time::Duration;
3
4use chrono::Utc;
5use dashmap::DashMap;
6use uuid::Uuid;
7
8use super::types::Session;
9
10type ExpireCallback = Arc<dyn Fn(Uuid) + Send + Sync>;
11
12pub const DEFAULT_MAX_SESSIONS: usize = 10_000;
19
20#[derive(Clone)]
21pub struct SessionManager {
22 sessions: Arc<DashMap<Uuid, Session>>,
23 ttl: Duration,
24 max_turns: usize,
25 max_sessions: usize,
26 on_expire: Arc<Mutex<Vec<ExpireCallback>>>,
29}
30
31impl SessionManager {
32 pub fn new(ttl: Duration, max_turns: usize) -> Self {
34 Self::with_cap(ttl, max_turns, DEFAULT_MAX_SESSIONS)
35 }
36
37 pub fn with_cap(ttl: Duration, max_turns: usize, max_sessions: usize) -> Self {
40 let sessions = Arc::new(DashMap::new());
41 let on_expire: Arc<Mutex<Vec<ExpireCallback>>> = Arc::new(Mutex::new(Vec::new()));
42 let mgr = Self {
43 sessions,
44 ttl,
45 max_turns,
46 max_sessions,
47 on_expire,
48 };
49 mgr.spawn_sweeper();
50 mgr
51 }
52
53 fn enforce_cap(&self) {
57 if self.max_sessions == 0 {
58 return;
59 }
60 while self.sessions.len() >= self.max_sessions {
61 let oldest = self
62 .sessions
63 .iter()
64 .min_by_key(|e| e.value().last_access)
65 .map(|e| *e.key());
66 match oldest {
67 Some(id) => {
68 if self.sessions.remove(&id).is_some() {
69 tracing::warn!(
70 session_id = %id,
71 cap = self.max_sessions,
72 "session cap reached; evicting oldest-idle"
73 );
74 self.fire_expire(id);
75 }
76 }
77 None => break,
78 }
79 }
80 }
81
82 pub fn create(&self, agent_id: impl Into<String>) -> Session {
83 self.enforce_cap();
84 let session = Session::new(agent_id, self.max_turns);
85 self.sessions.insert(session.id, session.clone());
86 session
87 }
88
89 pub fn get(&self, id: Uuid) -> Option<Session> {
91 let mut entry = self.sessions.get_mut(&id)?;
92 entry.last_access = Utc::now();
93 Some(entry.clone())
94 }
95
96 pub fn get_or_create(&self, id: Uuid, agent_id: impl Into<String>) -> Session {
98 if !self.sessions.contains_key(&id) {
102 self.enforce_cap();
103 }
104 let agent_id = agent_id.into();
105 let max_turns = self.max_turns;
106 let mut entry = self
107 .sessions
108 .entry(id)
109 .or_insert_with(|| Session::with_id(id, agent_id, max_turns));
110 entry.last_access = Utc::now();
111 entry.clone()
112 }
113
114 pub fn update(&self, session: Session) -> bool {
120 let Some(mut entry) = self.sessions.get_mut(&session.id) else {
121 return false;
122 };
123 *entry = session;
124 true
125 }
126
127 pub fn push_message(&self, id: Uuid, interaction: super::types::Interaction) -> bool {
134 let Some(mut entry) = self.sessions.get_mut(&id) else {
135 return false;
136 };
137 entry.push(interaction);
138 true
139 }
140
141 pub fn delete(&self, id: Uuid) -> bool {
144 let existed = self.sessions.remove(&id).is_some();
145 if existed {
146 self.fire_expire(id);
147 }
148 existed
149 }
150
151 pub fn active_count(&self) -> usize {
152 self.sessions.len()
153 }
154
155 pub fn on_expire<F>(&self, f: F)
162 where
163 F: Fn(Uuid) + Send + Sync + 'static,
164 {
165 self.on_expire.lock().unwrap().push(Arc::new(f));
166 }
167
168 fn fire_expire(&self, id: Uuid) {
169 let callbacks: Vec<ExpireCallback> = self.on_expire.lock().unwrap().clone();
170 spawn_callbacks(&callbacks, id);
171 }
172
173 fn spawn_sweeper(&self) {
174 let sessions = Arc::clone(&self.sessions);
175 let on_expire = Arc::clone(&self.on_expire);
176 let ttl = self.ttl;
177 let interval = (ttl / 4).max(Duration::from_millis(10));
179
180 tokio::spawn(async move {
181 let mut ticker = tokio::time::interval(interval);
182 ticker.tick().await; loop {
184 ticker.tick().await;
185 let now = Utc::now();
186 let ttl_chrono =
187 chrono::Duration::from_std(ttl).unwrap_or(chrono::Duration::hours(24));
188
189 let expired: Vec<Uuid> = sessions
192 .iter()
193 .filter_map(|entry| {
194 if now.signed_duration_since(entry.value().last_access) >= ttl_chrono {
195 Some(*entry.key())
196 } else {
197 None
198 }
199 })
200 .collect();
201 if expired.is_empty() {
202 continue;
203 }
204 for id in &expired {
205 sessions.remove(id);
206 }
207 let callbacks: Vec<ExpireCallback> = on_expire.lock().unwrap().clone();
208 if callbacks.is_empty() {
209 continue;
210 }
211 for id in expired {
212 spawn_callbacks(&callbacks, id);
213 }
214 }
215 });
216 }
217}
218
219fn spawn_callbacks(callbacks: &[ExpireCallback], id: Uuid) {
220 for cb in callbacks {
221 let cb = cb.clone();
222 tokio::spawn(async move {
223 cb(id);
224 });
225 }
226}