use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use futures::Stream;
use rmcp::{
model::{ClientJsonRpcMessage, ServerJsonRpcMessage},
transport::{
WorkerTransport,
common::server_side_http::ServerSseMessage,
streamable_http_server::session::{
SessionId, SessionManager,
local::{LocalSessionManager, LocalSessionManagerError, LocalSessionWorker},
},
},
};
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info, warn};
use super::proxy_handler::ProxyHandler;
#[derive(Debug, Clone)]
struct SessionMetadata {
backend_version: u64,
}
pub struct ProxyAwareSessionManager {
inner: LocalSessionManager,
handler: Arc<ProxyHandler>,
session_versions: DashMap<String, SessionMetadata>,
max_sessions: Option<usize>,
}
impl ProxyAwareSessionManager {
const DEFAULT_SESSION_KEEP_ALIVE_SECS: u64 = 30 * 60;
pub fn new(handler: Arc<ProxyHandler>) -> Self {
Self::with_keep_alive(
handler,
Duration::from_secs(Self::DEFAULT_SESSION_KEEP_ALIVE_SECS),
)
}
pub fn with_keep_alive(handler: Arc<ProxyHandler>, keep_alive: Duration) -> Self {
info!(
"[Session Manager] Create ProxyAwareSessionManager - MCP ID: {}, keep_alive: {}s",
handler.mcp_id(),
keep_alive.as_secs()
);
let mut inner = LocalSessionManager::default();
inner.session_config.keep_alive = Some(keep_alive);
Self {
inner,
handler,
session_versions: DashMap::new(),
max_sessions: None,
}
}
pub fn with_max_sessions(mut self, max_sessions: Option<usize>) -> Self {
self.max_sessions = max_sessions;
self
}
fn check_backend_version(&self, session_id: &SessionId) -> bool {
if self.handler.isolation() == crate::backend_connector::BackendIsolation::PerSession {
return true;
}
let current_version = self.handler.get_backend_version();
match self.session_versions.entry(session_id.to_string()) {
Entry::Occupied(entry) if entry.get().backend_version != current_version => {
warn!(
"[Session version mismatch] session_id={}, creation version={}, current version={}, MCP ID: {}",
session_id,
entry.get().backend_version,
current_version,
self.handler.mcp_id()
);
false
}
Entry::Occupied(_) | Entry::Vacant(_) => true,
}
}
fn backend_ready_for_session_ops(&self) -> bool {
match self.handler.isolation() {
crate::backend_connector::BackendIsolation::PerSession => true,
crate::backend_connector::BackendIsolation::Shared => {
self.handler.is_backend_available()
}
}
}
}
impl SessionManager for ProxyAwareSessionManager {
type Error = LocalSessionManagerError;
type Transport = WorkerTransport<LocalSessionWorker>;
async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> {
if let Some(max) = self.max_sessions {
let current = self.session_versions.len();
if current >= max {
warn!(
"[Session limit] rejecting create_session: {} >= max {}, MCP ID: {}",
current,
max,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(
format!("session-limit-{max}").into(),
));
}
}
let (session_id, transport) = self.inner.create_session().await?;
let version = self.handler.get_backend_version();
self.session_versions.insert(
session_id.to_string(),
SessionMetadata {
backend_version: version,
},
);
info!(
"[SessionCreated] session_id={}, backend_version={}, MCP ID: {}",
session_id,
version,
self.handler.mcp_id()
);
Ok((session_id, transport))
}
async fn initialize_session(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<ServerJsonRpcMessage, Self::Error> {
if !self.backend_ready_for_session_ops() {
warn!(
"[Session initialization failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
if !self.check_backend_version(id) {
warn!(
"[Session initialization failed] session_id={}, reason: version mismatch, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
debug!(
"[Session initialization] session_id={}, MCP ID: {}",
id,
self.handler.mcp_id()
);
self.inner.initialize_session(id, message).await
}
async fn has_session(&self, id: &SessionId) -> Result<bool, Self::Error> {
if !self.check_backend_version(id) {
return Ok(false);
}
self.inner.has_session(id).await
}
async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
info!(
"[SessionClosed] session_id={}, MCP ID: {}",
id,
self.handler.mcp_id()
);
self.session_versions.remove(id.as_ref());
self.handler.upstream_peers().reap_closed_peers();
self.inner.close_session(id).await
}
async fn create_stream(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
if !self.backend_ready_for_session_ops() {
warn!(
"[Stream creation failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
if !self.check_backend_version(id) {
warn!(
"[Stream creation failed] session_id={}, reason: version mismatch, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
debug!(
"[Stream creation] session_id={}, MCP ID: {}",
id,
self.handler.mcp_id()
);
self.inner.create_stream(id, message).await
}
async fn accept_message(
&self,
id: &SessionId,
message: ClientJsonRpcMessage,
) -> Result<(), Self::Error> {
if !self.backend_ready_for_session_ops() {
warn!(
"[Message rejected] session_id={}, reason: backend unavailable, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
if !self.check_backend_version(id) {
warn!(
"[Message rejected] session_id={}, reason: version mismatch, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
self.inner.accept_message(id, message).await
}
async fn create_standalone_stream(
&self,
id: &SessionId,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
self.inner.create_standalone_stream(id).await
}
async fn resume(
&self,
id: &SessionId,
last_event_id: String,
) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
let current_version = self.handler.get_backend_version();
let stale_version =
remove_stale_session_version(&self.session_versions, id.as_ref(), current_version);
if let Some(previous_version) = stale_version {
warn!(
"[Session recovery failed] session_id={}, reason: backend version change ({} -> {}), MCP ID: {}",
id,
previous_version,
current_version,
self.handler.mcp_id()
);
let _ = self.inner.close_session(id).await;
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
if !self.backend_ready_for_session_ops() {
warn!(
"[Session recovery failed] session_id={}, reason: backend is unavailable, MCP ID: {}",
id,
self.handler.mcp_id()
);
return Err(LocalSessionManagerError::SessionNotFound(id.clone()));
}
debug!(
"[SessionResumed] session_id={}, last_event_id={}, MCP ID: {}",
id,
last_event_id,
self.handler.mcp_id()
);
self.inner.resume(id, last_event_id).await
}
}
fn remove_stale_session_version(
versions: &DashMap<String, SessionMetadata>,
session_id: &str,
current_version: u64,
) -> Option<u64> {
match versions.entry(session_id.to_owned()) {
Entry::Occupied(entry) if entry.get().backend_version != current_version => {
let previous = entry.get().backend_version;
entry.remove();
Some(previous)
}
Entry::Occupied(_) | Entry::Vacant(_) => None,
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Barrier};
use super::*;
#[test]
fn stale_version_removal_does_not_remove_replacement() {
let versions = DashMap::new();
versions.insert("session".to_owned(), SessionMetadata { backend_version: 1 });
assert_eq!(
remove_stale_session_version(&versions, "session", 2),
Some(1)
);
versions.insert("session".to_owned(), SessionMetadata { backend_version: 2 });
assert_eq!(remove_stale_session_version(&versions, "session", 2), None);
assert_eq!(
versions
.entry("session".to_owned())
.or_insert(SessionMetadata { backend_version: 0 })
.backend_version,
2
);
}
#[test]
fn concurrent_version_change_uses_atomic_entry_operations() {
let versions = Arc::new(DashMap::new());
versions.insert("session".to_owned(), SessionMetadata { backend_version: 1 });
let stale_removed = Arc::new(Barrier::new(2));
let replacement_inserted = Arc::new(Barrier::new(2));
let worker_versions = versions.clone();
let worker_stale_removed = stale_removed.clone();
let worker_replacement_inserted = replacement_inserted.clone();
let worker = std::thread::spawn(move || {
let removed = remove_stale_session_version(&worker_versions, "session", 2);
worker_stale_removed.wait();
worker_replacement_inserted.wait();
let second_removal = remove_stale_session_version(&worker_versions, "session", 2);
(removed, second_removal)
});
stale_removed.wait();
versions.insert("session".to_owned(), SessionMetadata { backend_version: 2 });
replacement_inserted.wait();
let (removed, second_removal) = worker.join().expect("worker thread must finish");
assert_eq!(removed, Some(1));
assert_eq!(second_removal, None);
assert_eq!(
versions
.entry("session".to_owned())
.or_insert(SessionMetadata { backend_version: 0 })
.backend_version,
2
);
}
}