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()
98            == crate::backend_connector::BackendIsolation::PerSession
99        {
100            return true;
101        }
102        if let Some(meta) = self.session_versions.get(session_id.as_ref()) {
103            let current_version = self.handler.get_backend_version();
104            if meta.backend_version != current_version {
105                warn!(
106                    "[Session version mismatch] session_id={}, creation version={}, current version={}, MCP ID: {}",
107                    session_id,
108                    meta.backend_version,
109                    current_version,
110                    self.handler.mcp_id()
111                );
112                return false;
113            }
114        }
115        true
116    }
117
118    fn backend_ready_for_session_ops(&self) -> bool {
119        match self.handler.isolation() {
120            crate::backend_connector::BackendIsolation::PerSession => true,
121            crate::backend_connector::BackendIsolation::Shared => {
122                self.handler.is_backend_available()
123            }
124        }
125    }
126}
127
128// Implement SessionManager trait
129impl SessionManager for ProxyAwareSessionManager {
130    type Error = LocalSessionManagerError;
131    type Transport = WorkerTransport<LocalSessionWorker>;
132
133    async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
134        if let Some(max) = self.max_sessions {
135            let current = self.session_versions.len();
136            if current >= max {
137                warn!(
138                    "[Session limit] rejecting create_session: {} >= max {}, MCP ID: {}",
139                    current,
140                    max,
141                    self.handler.mcp_id()
142                );
143                return Err(LocalSessionManagerError::SessionNotFound(
144                    format!("session-limit-{max}").into(),
145                ));
146            }
147        }
148
149        let (session_id, transport) = self.inner.create_session().await?;
150
151        let version = self.handler.get_backend_version();
152        self.session_versions.insert(
153            session_id.to_string(),
154            SessionMetadata {
155                backend_version: version,
156            },
157        );
158
159        info!(
160            "[SessionCreated] session_id={}, backend_version={}, MCP ID: {}",
161            session_id,
162            version,
163            self.handler.mcp_id()
164        );
165
166        Ok((session_id, transport))
167    }
168
169    async fn initialize_session(
170        &self,
171        id: &SessionId,
172        message: ClientJsonRpcMessage,
173    ) -> Result<ServerJsonRpcMessage, Self::Error> {
174        if !self.backend_ready_for_session_ops() {
175            warn!(
176                "[Session initialization failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
177                id,
178                self.handler.mcp_id()
179            );
180            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
181        }
182
183        if !self.check_backend_version(id) {
184            warn!(
185                "[Session initialization failed] session_id={}, reason: version mismatch, MCP ID: {}",
186                id,
187                self.handler.mcp_id()
188            );
189            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
190        }
191
192        debug!(
193            "[Session initialization] session_id={}, MCP ID: {}",
194            id,
195            self.handler.mcp_id()
196        );
197        self.inner.initialize_session(id, message).await
198    }
199
200    async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
201        if !self.check_backend_version(id) {
202            return Ok(false);
203        }
204        self.inner.has_session(id).await
205    }
206
207    async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
208        info!(
209            "[SessionClosed] session_id={}, MCP ID: {}",
210            id,
211            self.handler.mcp_id()
212        );
213        self.session_versions.remove(id.as_ref());
214        // Shared isolation: drop peers whose transport is already closed.
215        self.handler.upstream_peers().reap_closed_peers();
216        self.inner.close_session(id).await
217    }
218
219    async fn create_stream(
220        &self,
221        id: &SessionId,
222        message: ClientJsonRpcMessage,
223    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
224        if !self.backend_ready_for_session_ops() {
225            warn!(
226                "[Stream creation failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
227                id,
228                self.handler.mcp_id()
229            );
230            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
231        }
232
233        if !self.check_backend_version(id) {
234            warn!(
235                "[Stream creation failed] session_id={}, reason: version mismatch, MCP ID: {}",
236                id,
237                self.handler.mcp_id()
238            );
239            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
240        }
241
242        debug!(
243            "[Stream creation] session_id={}, MCP ID: {}",
244            id,
245            self.handler.mcp_id()
246        );
247        self.inner.create_stream(id, message).await
248    }
249
250    async fn accept_message(
251        &self,
252        id: &SessionId,
253        message: ClientJsonRpcMessage,
254    ) -> Result<(), Self::Error> {
255        if !self.backend_ready_for_session_ops() {
256            warn!(
257                "[Message rejected] session_id={}, reason: backend unavailable, MCP ID: {}",
258                id,
259                self.handler.mcp_id()
260            );
261            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
262        }
263
264        if !self.check_backend_version(id) {
265            warn!(
266                "[Message rejected] session_id={}, reason: version mismatch, MCP ID: {}",
267                id,
268                self.handler.mcp_id()
269            );
270            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
271        }
272
273        self.inner.accept_message(id, message).await
274    }
275
276    async fn create_standalone_stream(
277        &self,
278        id: &SessionId,
279    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
280        self.inner.create_standalone_stream(id).await
281    }
282
283    async fn resume(
284        &self,
285        id: &SessionId,
286        last_event_id: String,
287    ) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
288        // 关键:检查后端版本
289        if let Some(meta) = self.session_versions.get(id.as_ref()) {
290            let current_version = self.handler.get_backend_version();
291            if meta.backend_version != current_version {
292                warn!(
293                    "[Session recovery failed] session_id={}, reason: backend version change ({} -> {}), MCP ID: {}",
294                    id,
295                    meta.backend_version,
296                    current_version,
297                    self.handler.mcp_id()
298                );
299
300                // 清理失效 session
301                drop(meta); // 释放 DashMap 的读锁
302                self.session_versions.remove(id.as_ref());
303                let _ = self.inner.close_session(id).await;
304
305                return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
306            }
307        }
308
309        if !self.backend_ready_for_session_ops() {
310            warn!(
311                "[Session recovery failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
312                id,
313                self.handler.mcp_id()
314            );
315            return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
316        }
317
318        debug!(
319            "[SessionResumed] session_id={}, last_event_id={}, MCP ID: {}",
320            id,
321            last_event_id,
322            self.handler.mcp_id()
323        );
324        self.inner.resume(id, last_event_id).await
325    }
326}