Skip to content

Commit 1de7ee4

Browse files
fix(streamable-http-server): release the session map lock before waiting on a session
LocalSessionManager kept the read guard on its session map while it waited on a session worker. initialize_session keeps it for the whole ServerHandler::initialize, so one slow initialize blocks create_session for every new client. tokio's RwLock is fair, so has_session calls for other sessions then wait behind that writer, and the whole server waits until the slow initialize returns. Clone the session handle out of the map and drop the guard before waiting, like close_session already does.
1 parent 79437f2 commit 1de7ee4

2 files changed

Lines changed: 137 additions & 20 deletions

File tree

‎crates/rmcp/src/transport/streamable_http_server/session/local.rs‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ impl LocalSessionManager {
4242
self.event_store = Some(event_store);
4343
self
4444
}
45+
46+
/// Clone the session handle out of the map, so the lock is released
47+
/// before the caller waits on the session worker.
48+
async fn session_handle(
49+
&self,
50+
id: &SessionId,
51+
) -> Result<LocalSessionHandle, LocalSessionManagerError> {
52+
self.sessions
53+
.read()
54+
.await
55+
.get(id)
56+
.cloned()
57+
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))
58+
}
4559
}
4660

4761
#[derive(Debug, Error)]
@@ -72,10 +86,7 @@ impl SessionManager for LocalSessionManager {
7286
id: &SessionId,
7387
message: ClientJsonRpcMessage,
7488
) -> Result<ServerJsonRpcMessage, Self::Error> {
75-
let sessions = self.sessions.read().await;
76-
let handle = sessions
77-
.get(id)
78-
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?;
89+
let handle = self.session_handle(id).await?;
7990
let response = handle.initialize(message).await?;
8091
Ok(response)
8192
}
@@ -102,10 +113,7 @@ impl SessionManager for LocalSessionManager {
102113
id: &SessionId,
103114
message: ClientJsonRpcMessage,
104115
) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
105-
let sessions = self.sessions.read().await;
106-
let handle = sessions
107-
.get(id)
108-
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?;
116+
let handle = self.session_handle(id).await?;
109117
let receiver = handle.establish_request_wise_channel().await?;
110118
let http_request_id = receiver.http_request_id;
111119
handle.push_message(message, http_request_id).await?;
@@ -116,10 +124,7 @@ impl SessionManager for LocalSessionManager {
116124
&self,
117125
id: &SessionId,
118126
) -> Result<impl Stream<Item = ServerSseMessage> + Send + 'static, Self::Error> {
119-
let sessions = self.sessions.read().await;
120-
let handle = sessions
121-
.get(id)
122-
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?;
127+
let handle = self.session_handle(id).await?;
123128
let receiver = handle.establish_common_channel().await?;
124129
Ok(ReceiverStream::new(receiver.inner))
125130
}
@@ -136,10 +141,7 @@ impl SessionManager for LocalSessionManager {
136141
.map_err(SessionError::EventStore)?;
137142
return Ok(stream.left_stream());
138143
}
139-
let sessions = self.sessions.read().await;
140-
let handle = sessions
141-
.get(id)
142-
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?;
144+
let handle = self.session_handle(id).await?;
143145
let receiver = handle.resume(last_event_id.parse()?).await?;
144146
Ok(ReceiverStream::new(receiver.inner).right_stream())
145147
}
@@ -149,10 +151,7 @@ impl SessionManager for LocalSessionManager {
149151
id: &SessionId,
150152
message: ClientJsonRpcMessage,
151153
) -> Result<(), Self::Error> {
152-
let sessions = self.sessions.read().await;
153-
let handle = sessions
154-
.get(id)
155-
.ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?;
154+
let handle = self.session_handle(id).await?;
156155
handle.push_message(message, None).await?;
157156
Ok(())
158157
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#![cfg(all(feature = "transport-streamable-http-server", not(feature = "local")))]
2+
3+
use std::{sync::Arc, time::Duration};
4+
5+
use rmcp::{
6+
model::{ClientJsonRpcMessage, ClientRequest, PingRequest, RequestId},
7+
transport::streamable_http_server::session::{
8+
SessionId, SessionManager,
9+
local::{LocalSessionManager, SessionConfig},
10+
},
11+
};
12+
use rstest::rstest;
13+
use tokio::task::JoinHandle;
14+
15+
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
16+
17+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18+
enum StuckCall {
19+
Initialize,
20+
CreateStream,
21+
CreateStandaloneStream,
22+
Resume,
23+
AcceptMessage,
24+
}
25+
26+
fn ping() -> ClientJsonRpcMessage {
27+
ClientJsonRpcMessage::request(
28+
ClientRequest::PingRequest(PingRequest::default()),
29+
RequestId::Number(1),
30+
)
31+
}
32+
33+
fn spawn_call(
34+
manager: &Arc<LocalSessionManager>,
35+
id: &SessionId,
36+
call: StuckCall,
37+
) -> JoinHandle<()> {
38+
let manager = manager.clone();
39+
let id = id.clone();
40+
tokio::spawn(async move {
41+
match call {
42+
StuckCall::Initialize => {
43+
let _ = manager.initialize_session(&id, ping()).await;
44+
}
45+
StuckCall::CreateStream => {
46+
let _ = manager.create_stream(&id, ping()).await;
47+
}
48+
StuckCall::CreateStandaloneStream => {
49+
let _ = manager.create_standalone_stream(&id).await;
50+
}
51+
StuckCall::Resume => {
52+
let _ = manager.resume(&id, "0".to_owned()).await;
53+
}
54+
StuckCall::AcceptMessage => {
55+
// The worker no longer reads its event channel, so the push
56+
// after the channel is full waits.
57+
for _ in 0..=SessionConfig::DEFAULT_CHANNEL_CAPACITY {
58+
let _ = manager.accept_message(&id, ping()).await;
59+
}
60+
}
61+
}
62+
})
63+
}
64+
65+
/// While a call waits on the worker of one session, the manager must still
66+
/// create new sessions and look up other sessions.
67+
#[rstest]
68+
#[case::initialize(StuckCall::Initialize)]
69+
#[case::create_stream(StuckCall::CreateStream)]
70+
#[case::create_standalone_stream(StuckCall::CreateStandaloneStream)]
71+
#[case::resume(StuckCall::Resume)]
72+
#[case::accept_message(StuckCall::AcceptMessage)]
73+
#[tokio::test]
74+
async fn waiting_on_one_session_does_not_block_other_sessions(
75+
#[case] call: StuckCall,
76+
) -> anyhow::Result<()> {
77+
let manager = Arc::new(LocalSessionManager::default());
78+
let (other_id, _other_transport) = manager.create_session().await?;
79+
// Nothing serves this transport, so the worker never answers initialize
80+
// and stops reading session events. Keep the transport bound: dropping it
81+
// cancels the worker.
82+
let (stuck_id, _stuck_transport) = manager.create_session().await?;
83+
84+
let mut stuck = vec![spawn_call(&manager, &stuck_id, StuckCall::Initialize)];
85+
if call != StuckCall::Initialize {
86+
tokio::time::sleep(Duration::from_millis(50)).await;
87+
stuck.push(spawn_call(&manager, &stuck_id, call));
88+
}
89+
tokio::time::sleep(Duration::from_millis(100)).await;
90+
91+
// A new client connects while the call is waiting...
92+
let create = tokio::spawn({
93+
let manager = manager.clone();
94+
async move { manager.create_session().await.map(|(id, _)| id) }
95+
});
96+
tokio::time::sleep(Duration::from_millis(100)).await;
97+
98+
// ...and a request arrives on another session.
99+
let lookup = tokio::time::timeout(PROBE_TIMEOUT, manager.has_session(&other_id)).await;
100+
assert!(
101+
matches!(lookup, Ok(Ok(true))),
102+
"has_session blocked: {lookup:?}"
103+
);
104+
let created = tokio::time::timeout(PROBE_TIMEOUT, create).await;
105+
assert!(
106+
matches!(created, Ok(Ok(Ok(_)))),
107+
"create_session blocked: {created:?}"
108+
);
109+
110+
for task in stuck {
111+
assert!(
112+
!task.is_finished(),
113+
"the call on the stuck session should still be waiting"
114+
);
115+
task.abort();
116+
}
117+
Ok(())
118+
}

0 commit comments

Comments
 (0)