#![cfg(all(feature = "transport-streamable-http-server", not(feature = "local")))]
use std::{sync::Arc, time::Duration};
use rmcp::{
model::{
ClientJsonRpcMessage, ClientRequest, PingRequest, RequestId, ServerJsonRpcMessage,
ServerResult,
},
transport::{
Transport,
streamable_http_server::session::{SessionId, SessionManager, local::LocalSessionManager},
},
};
use rstest::rstest;
use tokio::task::JoinHandle;
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StuckCall {
Initialize,
CreateStream,
CreateStandaloneStream,
Resume,
AcceptMessage,
}
fn ping() -> ClientJsonRpcMessage {
ClientJsonRpcMessage::request(
ClientRequest::PingRequest(PingRequest::default()),
RequestId::Number(1),
)
}
async fn settle() {
tokio::time::sleep(Duration::from_millis(1)).await;
}
fn spawn_call(
manager: &Arc<LocalSessionManager>,
id: &SessionId,
call: StuckCall,
) -> JoinHandle<()> {
let manager = manager.clone();
let id = id.clone();
tokio::spawn(async move {
match call {
StuckCall::Initialize => {
let _ = manager.initialize_session(&id, ping()).await;
}
StuckCall::CreateStream => {
let _ = manager.create_stream(&id, ping()).await;
}
StuckCall::CreateStandaloneStream => {
let _ = manager.create_standalone_stream(&id).await;
}
StuckCall::Resume => {
let _ = manager.resume(&id, "0".to_owned()).await;
}
StuckCall::AcceptMessage => {
for _ in 0..=manager.session_config.channel_capacity {
let _ = manager.accept_message(&id, ping()).await;
}
}
}
})
}
#[rstest]
#[case::initialize(StuckCall::Initialize)]
#[case::create_stream(StuckCall::CreateStream)]
#[case::create_standalone_stream(StuckCall::CreateStandaloneStream)]
#[case::resume(StuckCall::Resume)]
#[case::accept_message(StuckCall::AcceptMessage)]
#[tokio::test(start_paused = true)]
async fn waiting_on_one_session_does_not_block_other_sessions(
#[case] call: StuckCall,
) -> anyhow::Result<()> {
let manager = Arc::new(LocalSessionManager::default());
let (other_id, _other_transport) = manager.create_session().await?;
let (stuck_id, mut stuck_transport) = manager.create_session().await?;
let mut stuck = vec![spawn_call(&manager, &stuck_id, StuckCall::Initialize)];
assert!(
stuck_transport.receive().await.is_some(),
"the worker did not pass initialize on"
);
if call != StuckCall::Initialize {
stuck.push(spawn_call(&manager, &stuck_id, call));
}
settle().await;
let create = tokio::spawn({
let manager = manager.clone();
async move { manager.create_session().await.map(|(id, _)| id) }
});
settle().await;
let lookup = tokio::time::timeout(PROBE_TIMEOUT, manager.has_session(&other_id)).await;
assert!(
matches!(lookup, Ok(Ok(true))),
"has_session blocked: {lookup:?}"
);
let created = tokio::time::timeout(PROBE_TIMEOUT, create).await;
assert!(
matches!(created, Ok(Ok(Ok(_)))),
"create_session blocked: {created:?}"
);
for task in stuck {
assert!(
!task.is_finished(),
"the call on the stuck session should still be waiting"
);
task.abort();
}
Ok(())
}
#[tokio::test(start_paused = true)]
async fn close_session_does_not_wait_for_pending_initialize() -> anyhow::Result<()> {
let manager = Arc::new(LocalSessionManager::default());
let (id, mut transport) = manager.create_session().await?;
let initialize = tokio::spawn({
let manager = manager.clone();
let id = id.clone();
async move { manager.initialize_session(&id, ping()).await }
});
assert!(
transport.receive().await.is_some(),
"the worker did not pass initialize on"
);
let closed = tokio::time::timeout(PROBE_TIMEOUT, manager.close_session(&id)).await;
assert!(
matches!(closed, Ok(Ok(()))),
"close_session blocked: {closed:?}"
);
assert!(
!manager.has_session(&id).await?,
"the closed session is still in the map"
);
settle().await;
assert!(
!initialize.is_finished(),
"initialize should still wait for the handler"
);
transport
.send(ServerJsonRpcMessage::response(
ServerResult::empty(()),
RequestId::Number(1),
))
.await?;
let response = tokio::time::timeout(PROBE_TIMEOUT, initialize).await;
assert!(
matches!(response, Ok(Ok(Ok(ServerJsonRpcMessage::Response(_))))),
"initialize did not get the handler's response: {response:?}"
);
Ok(())
}