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 futures::Stream;
24use rmcp::{
25    model::{ClientJsonRpcMessage, ServerJsonRpcMessage},
26    transport::{
27        WorkerTransport,
28        common::server_side_http::ServerSseMessage,
29        streamable_http_server::session::{
30            SessionId, SessionManager,
31            local::{LocalSessionManager, LocalSessionManagerError, LocalSessionWorker},
32        },
33    },
34};
35use std::sync::Arc;
36use std::time::Duration;
37use tracing::{debug, info, warn};
38
39use super::proxy_handler::ProxyHandler;
40
41/// Session 元数据:跟踪 session 创建时的后端版本
42#[derive(Debug, Clone)]
43struct SessionMetadata {
44    backend_version: u64,
45}
46
47/// 感知代理状态的 SessionManager
48///
49/// 职责:
50/// 1. 委托 LocalSessionManager 处理核心 session 逻辑
51/// 2. 维护 session → backend_version 映射
52/// 3. 在 resume 时检查版本一致性
53/// 4. 版本不匹配时使 session 失效
54pub struct ProxyAwareSessionManager {
55    inner: LocalSessionManager,
56    handler: Arc<ProxyHandler>,
57    session_versions: DashMap<String, SessionMetadata>,
58    max_sessions: Option<usize>,
59}
60
61impl ProxyAwareSessionManager {
62    /// 默认 session keep_alive 超时: 30 分钟
63    /// rmcp 默认 5 分钟太短, 对于代理场景, agent 可能长时间不发消息但仍需要保持 session
64    const DEFAULT_SESSION_KEEP_ALIVE_SECS: u64 = 30 * 60;
65
66    pub fn new(handler: Arc<ProxyHandler>) -> Self {
67        Self::with_keep_alive(
68            handler,
69            Duration::from_secs(Self::DEFAULT_SESSION_KEEP_ALIVE_SECS),
70        )
71    }
72
73    pub fn with_keep_alive(handler: Arc<ProxyHandler>, keep_alive: Duration) -> Self {
74        info!(
75            "[Session Manager] Create ProxyAwareSessionManager - MCP ID: {}, keep_alive: {}s",
76            handler.mcp_id(),
77            keep_alive.as_secs()
78        );
79        let mut inner = LocalSessionManager::default();
80        inner.session_config.keep_alive = Some(keep_alive);
81        Self {
82            inner,
83            handler,
84            session_versions: DashMap::new(),
85            max_sessions: None,
86        }
87    }
88
89    /// Limit the number of concurrent sessions (`None` = unlimited).
90    pub fn with_max_sessions(mut self, max_sessions: Option<usize>) -> Self {
91        self.max_sessions = max_sessions;
92        self
93    }
94
95    fn check_backend_version(&self, session_id: &SessionId) -> bool {
96        // Per-session handlers own their own backend; template version is not authoritative.
97        if self.handler.isolation() == crate::backend_connector::BackendIsolation::PerSession {
98            return true;
99        }
100        if let Some(meta) = self.session_versions.get(session_id.as_ref()) {
101            let current_version = self.handler.get_backend_version();
102            if meta.backend_version != current_version {
103                warn!(
104                    "[Session version mismatch] session_id={}, creation version={}, current version={}, MCP ID: {}",
105                    session_id,
106                    meta.backend_version,
107                    current_version,
108                    self.handler.mcp_id()
109                );
110                return false;
111            }
112        }
113        true
114    }
115
116    fn backend_ready_for_session_ops(&self) -> bool {
117        match self.handler.isolation() {
118            crate::backend_connector::BackendIsolation::PerSession => true,
119            crate::backend_connector::BackendIsolation::Shared => {
120                self.handler.is_backend_available()
121            }
122        }
123    }
124}
125
126// Implement SessionManager trait
127impl SessionManager for ProxyAwareSessionManager {
128    type Error = LocalSessionManagerError;
129    type Transport = WorkerTransport<LocalSessionWorker>;
130
131    async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
132        if let Some(max) = self.max_sessions {
133            let current = self.session_versions.len();
134            if current >= max {
135                warn!(
136                    "[Session limit] rejecting create_session: {} >= max {}, MCP ID: {}",
137                    current,
138                    max,
139                    self.handler.mcp_id()
140                );
141                return Err(LocalSessionManagerError::SessionNotFound(
142                    format!("session-limit-{max}").into(),
143                ));
144            }
145        }
146
147        let (session_id, transport) = self.inner.create_session().await?;
148
149        let version = self.handler.get_backend_version();
150        self.session_versions.insert(
151            session_id.to_string(),
152            SessionMetadata {
153                backend_version: version,
154            },
155        );
156
157        info!(
158            "[SessionCreated] session_id={}, backend_version={}, MCP ID: {}",
159            session_id,
160            version,
161            self.handler.mcp_id()
162        );
163
164        Ok((session_id, transport))
165    }
166
167    async fn initialize_session(
168        &self,
169        id: &SessionId,
170        message: ClientJsonRpcMessage,
171    ) -> Result<ServerJsonRpcMessage, Self::Error> {
172        if !self.backend_ready_for_session_ops() {
173            warn!(
174                "[Session initialization failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
175                id,
176                self.handler.mcp_id()
177            );
178            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
179        }
180
181        if !self.check_backend_version(id) {
182            warn!(
183                "[Session initialization failed] session_id={}, reason: version mismatch, MCP ID: {}",
184                id,
185                self.handler.mcp_id()
186            );
187            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
188        }
189
190        debug!(
191            "[Session initialization] session_id={}, MCP ID: {}",
192            id,
193            self.handler.mcp_id()
194        );
195        self.inner.initialize_session(id, message).await
196    }
197
198    async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
199        if !self.check_backend_version(id) {
200            return Ok(false);
201        }
202        self.inner.has_session(id).await
203    }
204
205    async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
206        info!(
207            "[SessionClosed] session_id={}, MCP ID: {}",
208            id,
209            self.handler.mcp_id()
210        );
211        self.session_versions.remove(id.as_ref());
212        // Shared isolation: drop peers whose transport is already closed.
213        self.handler.upstream_peers().reap_closed_peers();
214        self.inner.close_session(id).await
215    }
216
217    async fn create_stream(
218        &self,
219        id: &SessionId,
220        message: ClientJsonRpcMessage,
221    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
222        if !self.backend_ready_for_session_ops() {
223            warn!(
224                "[Stream creation failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
225                id,
226                self.handler.mcp_id()
227            );
228            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
229        }
230
231        if !self.check_backend_version(id) {
232            warn!(
233                "[Stream creation failed] session_id={}, reason: version mismatch, MCP ID: {}",
234                id,
235                self.handler.mcp_id()
236            );
237            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
238        }
239
240        debug!(
241            "[Stream creation] session_id={}, MCP ID: {}",
242            id,
243            self.handler.mcp_id()
244        );
245        self.inner.create_stream(id, message).await
246    }
247
248    async fn accept_message(
249        &self,
250        id: &SessionId,
251        message: ClientJsonRpcMessage,
252    ) -> Result<(), Self::Error> {
253        if !self.backend_ready_for_session_ops() {
254            warn!(
255                "[Message rejected] session_id={}, reason: backend unavailable, MCP ID: {}",
256                id,
257                self.handler.mcp_id()
258            );
259            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
260        }
261
262        if !self.check_backend_version(id) {
263            warn!(
264                "[Message rejected] session_id={}, reason: version mismatch, MCP ID: {}",
265                id,
266                self.handler.mcp_id()
267            );
268            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
269        }
270
271        self.inner.accept_message(id, message).await
272    }
273
274    async fn create_standalone_stream(
275        &self,
276        id: &SessionId,
277    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
278        self.inner.create_standalone_stream(id).await
279    }
280
281    async fn resume(
282        &self,
283        id: &SessionId,
284        last_event_id: String,
285    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
286        // 关键:检查后端版本
287        if let Some(meta) = self.session_versions.get(id.as_ref()) {
288            let current_version = self.handler.get_backend_version();
289            if meta.backend_version != current_version {
290                warn!(
291                    "[Session recovery failed] session_id={}, reason: backend version change ({} -> {}), MCP ID: {}",
292                    id,
293                    meta.backend_version,
294                    current_version,
295                    self.handler.mcp_id()
296                );
297
298                // 清理失效 session
299                drop(meta); // 释放 DashMap 的读锁
300                self.session_versions.remove(id.as_ref());
301                let _ = self.inner.close_session(id).await;
302
303                return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
304            }
305        }
306
307        if !self.backend_ready_for_session_ops() {
308            warn!(
309                "[Session recovery failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
310                id,
311                self.handler.mcp_id()
312            );
313            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
314        }
315
316        debug!(
317            "[SessionResumed] session_id={}, last_event_id={}, MCP ID: {}",
318            id,
319            last_event_id,
320            self.handler.mcp_id()
321        );
322        self.inner.resume(id, last_event_id).await
323    }
324}