ag_agent/app_server/
registry.rs1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6use tokio_util::sync::CancellationToken;
7
8use crate::app_server::AppServerError;
9
10pub struct AppServerSessionRegistry<Runtime> {
16 active_turn_cancellations: Arc<Mutex<HashMap<String, CancellationToken>>>,
17 provider_name: &'static str,
18 sessions: Arc<Mutex<HashMap<String, Runtime>>>,
19}
20
21impl<Runtime> AppServerSessionRegistry<Runtime> {
22 pub fn new(provider_name: &'static str) -> Self {
24 Self {
25 active_turn_cancellations: Arc::new(Mutex::new(HashMap::new())),
26 provider_name,
27 sessions: Arc::new(Mutex::new(HashMap::new())),
28 }
29 }
30
31 pub fn take_session(&self, session_id: &str) -> Result<Option<Runtime>, AppServerError> {
36 let mut sessions = self
37 .sessions
38 .lock()
39 .map_err(|_| AppServerError::LockPoisoned {
40 provider: self.provider_name,
41 })?;
42
43 Ok(sessions.remove(session_id))
44 }
45
46 pub fn store_session(
51 &self,
52 session_id: String,
53 session: Runtime,
54 ) -> Result<(), AppServerError> {
55 let mut sessions = self
56 .sessions
57 .lock()
58 .map_err(|_| AppServerError::LockPoisoned {
59 provider: self.provider_name,
60 })?;
61 sessions.insert(session_id, session);
62
63 Ok(())
64 }
65
66 pub fn store_session_or_recover(
75 &self,
76 session_id: String,
77 session: Runtime,
78 ) -> Result<(), (AppServerError, Runtime)> {
79 let Ok(mut sessions) = self.sessions.lock() else {
80 return Err((
81 AppServerError::LockPoisoned {
82 provider: self.provider_name,
83 },
84 session,
85 ));
86 };
87 sessions.insert(session_id, session);
88
89 Ok(())
90 }
91
92 pub fn provider_name(&self) -> &'static str {
94 self.provider_name
95 }
96
97 pub fn register_active_turn(
108 &self,
109 session_id: &str,
110 ) -> Result<ActiveAppServerTurn, AppServerError> {
111 let token = CancellationToken::new();
112 let mut active_turn_cancellations =
113 self.active_turn_cancellations
114 .lock()
115 .map_err(|_| AppServerError::LockPoisoned {
116 provider: self.provider_name,
117 })?;
118 active_turn_cancellations.insert(session_id.to_string(), token.clone());
119
120 Ok(ActiveAppServerTurn {
121 active_turn_cancellations: Arc::clone(&self.active_turn_cancellations),
122 session_id: session_id.to_string(),
123 token,
124 })
125 }
126
127 pub fn cancel_active_turn(&self, session_id: &str) -> Result<bool, AppServerError> {
133 let active_turn_cancellations =
134 self.active_turn_cancellations
135 .lock()
136 .map_err(|_| AppServerError::LockPoisoned {
137 provider: self.provider_name,
138 })?;
139
140 let Some(token) = active_turn_cancellations.get(session_id) else {
141 return Ok(false);
142 };
143
144 token.cancel();
145
146 Ok(true)
147 }
148}
149
150impl<Runtime> Clone for AppServerSessionRegistry<Runtime> {
152 fn clone(&self) -> Self {
153 Self {
154 active_turn_cancellations: Arc::clone(&self.active_turn_cancellations),
155 provider_name: self.provider_name,
156 sessions: Arc::clone(&self.sessions),
157 }
158 }
159}
160
161pub struct ActiveAppServerTurn {
163 active_turn_cancellations: Arc<Mutex<HashMap<String, CancellationToken>>>,
164 session_id: String,
165 token: CancellationToken,
166}
167
168impl ActiveAppServerTurn {
169 pub fn token(&self) -> CancellationToken {
171 self.token.clone()
172 }
173}
174
175impl Drop for ActiveAppServerTurn {
176 fn drop(&mut self) {
177 if let Ok(mut active_turn_cancellations) = self.active_turn_cancellations.lock() {
178 active_turn_cancellations.remove(&self.session_id);
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn new_returns_registry_with_provider_name() {
189 let registry = AppServerSessionRegistry::<String>::new("test-provider");
191
192 assert_eq!(registry.provider_name(), "test-provider");
194 }
195
196 #[test]
197 fn store_and_take_session_round_trips_runtime() {
198 let registry = AppServerSessionRegistry::<String>::new("test");
200 registry
201 .store_session("session-1".to_string(), "runtime-a".to_string())
202 .expect("store should succeed");
203
204 let taken = registry
206 .take_session("session-1")
207 .expect("take should succeed");
208
209 assert_eq!(taken, Some("runtime-a".to_string()));
211 }
212
213 #[test]
214 fn take_session_returns_none_for_missing_id() {
215 let registry = AppServerSessionRegistry::<String>::new("test");
217
218 let taken = registry
220 .take_session("missing")
221 .expect("take should succeed");
222
223 assert_eq!(taken, None);
225 }
226
227 #[test]
228 fn take_session_removes_entry_from_registry() {
229 let registry = AppServerSessionRegistry::<String>::new("test");
231 registry
232 .store_session("session-1".to_string(), "runtime-a".to_string())
233 .expect("store should succeed");
234 let _ = registry.take_session("session-1");
235
236 let second_take = registry
238 .take_session("session-1")
239 .expect("take should succeed");
240
241 assert_eq!(second_take, None);
243 }
244
245 #[test]
246 fn store_session_or_recover_stores_runtime_on_healthy_lock() {
247 let registry = AppServerSessionRegistry::<String>::new("test");
249
250 let result =
252 registry.store_session_or_recover("session-1".to_string(), "runtime-a".to_string());
253
254 assert!(result.is_ok());
256 let taken = registry
257 .take_session("session-1")
258 .expect("take should succeed");
259 assert_eq!(taken, Some("runtime-a".to_string()));
260 }
261
262 #[test]
263 fn clone_shares_underlying_session_map() {
264 let registry = AppServerSessionRegistry::<String>::new("test");
266 let cloned = registry.clone();
267 registry
268 .store_session("session-1".to_string(), "runtime-a".to_string())
269 .expect("store should succeed");
270
271 let taken = cloned
273 .take_session("session-1")
274 .expect("take on clone should succeed");
275
276 assert_eq!(taken, Some("runtime-a".to_string()));
278 }
279
280 #[test]
281 fn cancel_active_turn_signals_registered_token() {
282 let registry = AppServerSessionRegistry::<String>::new("test");
284 let active_turn = registry
285 .register_active_turn("session-1")
286 .expect("register should succeed");
287 let token = active_turn.token();
288
289 let did_cancel = registry
291 .cancel_active_turn("session-1")
292 .expect("cancel should succeed");
293
294 assert!(did_cancel);
296 assert!(token.is_cancelled());
297 }
298
299 #[test]
300 fn active_turn_guard_unregisters_on_drop() {
301 let registry = AppServerSessionRegistry::<String>::new("test");
303 let active_turn = registry
304 .register_active_turn("session-1")
305 .expect("register should succeed");
306
307 drop(active_turn);
309 let did_cancel = registry
310 .cancel_active_turn("session-1")
311 .expect("cancel should succeed");
312
313 assert!(!did_cancel);
315 }
316}