1use dashmap::DashMap;
23use dashmap::mapref::entry::Entry;
24use futures::Stream;
25use rmcp::{
26 model::{ClientJsonRpcMessage, ServerJsonRpcMessage},
27 transport::{
28 WorkerTransport,
29 common::server_side_http::ServerSseMessage,
30 streamable_http_server::session::{
31 SessionId, SessionManager,
32 local::{LocalSessionManager, LocalSessionManagerError, LocalSessionWorker},
33 },
34 },
35};
36use std::sync::Arc;
37use std::time::Duration;
38use tracing::{debug, info, warn};
39
40use super::proxy_handler::ProxyHandler;
41
42#[derive(Debug, Clone)]
44struct SessionMetadata {
45 backend_version: u64,
46}
47
48pub struct ProxyAwareSessionManager {
56 inner: LocalSessionManager,
57 handler: Arc<ProxyHandler>,
58 session_versions: DashMap<String, SessionMetadata>,
59 max_sessions: Option<usize>,
60}
61
62impl ProxyAwareSessionManager {
63 const DEFAULT_SESSION_KEEP_ALIVE_SECS: u64 = 30 * 60;
66
67 pub fn new(handler: Arc<ProxyHandler>) -> Self {
68 Self::with_keep_alive(
69 handler,
70 Duration::from_secs(Self::DEFAULT_SESSION_KEEP_ALIVE_SECS),
71 )
72 }
73
74 pub fn with_keep_alive(handler: Arc<ProxyHandler>, keep_alive: Duration) -> Self {
75 info!(
76 "[Session Manager] Create ProxyAwareSessionManager - MCP ID: {}, keep_alive: {}s",
77 handler.mcp_id(),
78 keep_alive.as_secs()
79 );
80 let mut inner = LocalSessionManager::default();
81 inner.session_config.keep_alive = Some(keep_alive);
82 Self {
83 inner,
84 handler,
85 session_versions: DashMap::new(),
86 max_sessions: None,
87 }
88 }
89
90 pub fn with_max_sessions(mut self, max_sessions: Option<usize>) -> Self {
92 self.max_sessions = max_sessions;
93 self
94 }
95
96 fn check_backend_version(&self, session_id: &SessionId) -> bool {
97 if self.handler.isolation() == crate::backend_connector::BackendIsolation::PerSession {
99 return true;
100 }
101 let current_version = self.handler.get_backend_version();
102 match self.session_versions.entry(session_id.to_string()) {
103 Entry::Occupied(entry) if entry.get().backend_version != current_version => {
104 warn!(
105 "[Session version mismatch] session_id={}, creation version={}, current version={}, MCP ID: {}",
106 session_id,
107 entry.get().backend_version,
108 current_version,
109 self.handler.mcp_id()
110 );
111 false
112 }
113 Entry::Occupied(_) | Entry::Vacant(_) => true,
114 }
115 }
116
117 fn backend_ready_for_session_ops(&self) -> bool {
118 match self.handler.isolation() {
119 crate::backend_connector::BackendIsolation::PerSession => true,
120 crate::backend_connector::BackendIsolation::Shared => {
121 self.handler.is_backend_available()
122 }
123 }
124 }
125}
126
127impl SessionManager for ProxyAwareSessionManager {
129 type Error = LocalSessionManagerError;
130 type Transport = WorkerTransport<LocalSessionWorker>;
131
132 async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
133 if let Some(max) = self.max_sessions {
134 let current = self.session_versions.len();
135 if current >= max {
136 warn!(
137 "[Session limit] rejecting create_session: {} >= max {}, MCP ID: {}",
138 current,
139 max,
140 self.handler.mcp_id()
141 );
142 return Err(LocalSessionManagerError::SessionNotFound(
143 format!("session-limit-{max}").into(),
144 ));
145 }
146 }
147
148 let (session_id, transport) = self.inner.create_session().await?;
149
150 let version = self.handler.get_backend_version();
151 self.session_versions.insert(
152 session_id.to_string(),
153 SessionMetadata {
154 backend_version: version,
155 },
156 );
157
158 info!(
159 "[SessionCreated] session_id={}, backend_version={}, MCP ID: {}",
160 session_id,
161 version,
162 self.handler.mcp_id()
163 );
164
165 Ok((session_id, transport))
166 }
167
168 async fn initialize_session(
169 &self,
170 id: &SessionId,
171 message: ClientJsonRpcMessage,
172 ) -> Result<ServerJsonRpcMessage, Self::Error> {
173 if !self.backend_ready_for_session_ops() {
174 warn!(
175 "[Session initialization failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
176 id,
177 self.handler.mcp_id()
178 );
179 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
180 }
181
182 if !self.check_backend_version(id) {
183 warn!(
184 "[Session initialization failed] session_id={}, reason: version mismatch, MCP ID: {}",
185 id,
186 self.handler.mcp_id()
187 );
188 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
189 }
190
191 debug!(
192 "[Session initialization] session_id={}, MCP ID: {}",
193 id,
194 self.handler.mcp_id()
195 );
196 self.inner.initialize_session(id, message).await
197 }
198
199 async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
200 if !self.check_backend_version(id) {
201 return Ok(false);
202 }
203 self.inner.has_session(id).await
204 }
205
206 async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
207 info!(
208 "[SessionClosed] session_id={}, MCP ID: {}",
209 id,
210 self.handler.mcp_id()
211 );
212 self.session_versions.remove(id.as_ref());
213 self.handler.upstream_peers().reap_closed_peers();
215 self.inner.close_session(id).await
216 }
217
218 async fn create_stream(
219 &self,
220 id: &SessionId,
221 message: ClientJsonRpcMessage,
222 ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
223 if !self.backend_ready_for_session_ops() {
224 warn!(
225 "[Stream creation failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
226 id,
227 self.handler.mcp_id()
228 );
229 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
230 }
231
232 if !self.check_backend_version(id) {
233 warn!(
234 "[Stream creation failed] session_id={}, reason: version mismatch, MCP ID: {}",
235 id,
236 self.handler.mcp_id()
237 );
238 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
239 }
240
241 debug!(
242 "[Stream creation] session_id={}, MCP ID: {}",
243 id,
244 self.handler.mcp_id()
245 );
246 self.inner.create_stream(id, message).await
247 }
248
249 async fn accept_message(
250 &self,
251 id: &SessionId,
252 message: ClientJsonRpcMessage,
253 ) -> Result<(), Self::Error> {
254 if !self.backend_ready_for_session_ops() {
255 warn!(
256 "[Message rejected] session_id={}, reason: backend unavailable, MCP ID: {}",
257 id,
258 self.handler.mcp_id()
259 );
260 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
261 }
262
263 if !self.check_backend_version(id) {
264 warn!(
265 "[Message rejected] session_id={}, reason: version mismatch, MCP ID: {}",
266 id,
267 self.handler.mcp_id()
268 );
269 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
270 }
271
272 self.inner.accept_message(id, message).await
273 }
274
275 async fn create_standalone_stream(
276 &self,
277 id: &SessionId,
278 ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
279 self.inner.create_standalone_stream(id).await
280 }
281
282 async fn resume(
283 &self,
284 id: &SessionId,
285 last_event_id: String,
286 ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
287 let current_version = self.handler.get_backend_version();
288 let stale_version =
290 remove_stale_session_version(&self.session_versions, id.as_ref(), current_version);
291 if let Some(previous_version) = stale_version {
292 warn!(
293 "[Session recovery failed] session_id={}, reason: backend version change ({} -> {}), MCP ID: {}",
294 id,
295 previous_version,
296 current_version,
297 self.handler.mcp_id()
298 );
299 let _ = self.inner.close_session(id).await;
300 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
301 }
302
303 if !self.backend_ready_for_session_ops() {
304 warn!(
305 "[Session recovery failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
306 id,
307 self.handler.mcp_id()
308 );
309 return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
310 }
311
312 debug!(
313 "[SessionResumed] session_id={}, last_event_id={}, MCP ID: {}",
314 id,
315 last_event_id,
316 self.handler.mcp_id()
317 );
318 self.inner.resume(id, last_event_id).await
319 }
320}
321
322fn remove_stale_session_version(
323 versions: &DashMap<String, SessionMetadata>,
324 session_id: &str,
325 current_version: u64,
326) -> Option<u64> {
327 match versions.entry(session_id.to_owned()) {
328 Entry::Occupied(entry) if entry.get().backend_version != current_version => {
329 let previous = entry.get().backend_version;
330 entry.remove();
331 Some(previous)
332 }
333 Entry::Occupied(_) | Entry::Vacant(_) => None,
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use std::sync::{Arc, Barrier};
340
341 use super::*;
342
343 #[test]
344 fn stale_version_removal_does_not_remove_replacement() {
345 let versions = DashMap::new();
346 versions.insert("session".to_owned(), SessionMetadata { backend_version: 1 });
347
348 assert_eq!(
349 remove_stale_session_version(&versions, "session", 2),
350 Some(1)
351 );
352 versions.insert("session".to_owned(), SessionMetadata { backend_version: 2 });
353
354 assert_eq!(remove_stale_session_version(&versions, "session", 2), None);
355 assert_eq!(
356 versions
357 .entry("session".to_owned())
358 .or_insert(SessionMetadata { backend_version: 0 })
359 .backend_version,
360 2
361 );
362 }
363
364 #[test]
365 fn concurrent_version_change_uses_atomic_entry_operations() {
366 let versions = Arc::new(DashMap::new());
367 versions.insert("session".to_owned(), SessionMetadata { backend_version: 1 });
368 let stale_removed = Arc::new(Barrier::new(2));
369 let replacement_inserted = Arc::new(Barrier::new(2));
370
371 let worker_versions = versions.clone();
372 let worker_stale_removed = stale_removed.clone();
373 let worker_replacement_inserted = replacement_inserted.clone();
374 let worker = std::thread::spawn(move || {
375 let removed = remove_stale_session_version(&worker_versions, "session", 2);
376 worker_stale_removed.wait();
377 worker_replacement_inserted.wait();
378 let second_removal = remove_stale_session_version(&worker_versions, "session", 2);
379 (removed, second_removal)
380 });
381
382 stale_removed.wait();
383 versions.insert("session".to_owned(), SessionMetadata { backend_version: 2 });
384 replacement_inserted.wait();
385
386 let (removed, second_removal) = worker.join().expect("worker thread must finish");
387 assert_eq!(removed, Some(1));
388 assert_eq!(second_removal, None);
389 assert_eq!(
390 versions
391 .entry("session".to_owned())
392 .or_insert(SessionMetadata { backend_version: 0 })
393 .backend_version,
394 2
395 );
396 }
397}