Skip to main content

mcp_streamable_proxy/
session_manager.rs

1//! Session Manager with backend version tracking
2//!
3//! This module implements ProxyAwareSessionManager that integrates with
4//! ProxyHandler's version control mechanism to automatically invalidate
5//! sessions when the backend reconnects.
6//!
7//! # Architecture
8//!
9//! ```text
10//! ProxyAwareSessionManager
11//! ├── LocalSessionManager (rmcp 提供的基础实现)
12//! ├── ProxyHandler (Arc, 访问 backend_version)
13//! └── DashMap<SessionId, SessionMetadata> (跟踪 session 创建时的版本)
14//!
15//! 工作流程:
16//! 1. create_session: 记录当前 backend_version
17//! 2. resume: 检查版本是否匹配
18//!    - 匹配 → 正常 resume
19//!    - 不匹配 → 返回 NotFound,客户端重新创建 session
20//! ```
21
22use 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/// Session 元数据:跟踪 session 创建时的后端版本
43#[derive(Debug, Clone)]
44struct SessionMetadata {
45    backend_version: u64,
46}
47
48/// 感知代理状态的 SessionManager
49///
50/// 职责:
51/// 1. 委托 LocalSessionManager 处理核心 session 逻辑
52/// 2. 维护 session → backend_version 映射
53/// 3. 在 resume 时检查版本一致性
54/// 4. 版本不匹配时使 session 失效
55pub 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    /// 默认 session keep_alive 超时: 30 分钟
64    /// rmcp 默认 5 分钟太短, 对于代理场景, agent 可能长时间不发消息但仍需要保持 session
65    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    /// Limit the number of concurrent sessions (`None` = unlimited).
91    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        // Per-session handlers own their own backend; template version is not authoritative.
98        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
127// Implement SessionManager trait
128impl 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        // Shared isolation: drop peers whose transport is already closed.
214        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        // This entry guard is dropped before close_session().await below.
289        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}