fix(streamable-http): gracefully shutdown while client connected (#494)
* fix(streamable-http): gracefully shutdown while client connected * fix: adviced comments * fix: windows test build
This commit is contained in:
parent
57d1ac94a0
commit
3dee024325
5 changed files with 52 additions and 15 deletions
|
|
@ -6,6 +6,7 @@ use http::Response;
|
||||||
use http_body::Body;
|
use http_body::Body;
|
||||||
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
|
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
|
||||||
use sse_stream::{KeepAlive, Sse, SseBody};
|
use sse_stream::{KeepAlive, Sse, SseBody};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use super::http_header::EVENT_STREAM_MIME_TYPE;
|
use super::http_header::EVENT_STREAM_MIME_TYPE;
|
||||||
use crate::model::{ClientJsonRpcMessage, ServerJsonRpcMessage};
|
use crate::model::{ClientJsonRpcMessage, ServerJsonRpcMessage};
|
||||||
|
|
@ -65,20 +66,26 @@ pub struct ServerSseMessage {
|
||||||
pub(crate) fn sse_stream_response(
|
pub(crate) fn sse_stream_response(
|
||||||
stream: impl futures::Stream<Item = ServerSseMessage> + Send + Sync + 'static,
|
stream: impl futures::Stream<Item = ServerSseMessage> + Send + Sync + 'static,
|
||||||
keep_alive: Option<Duration>,
|
keep_alive: Option<Duration>,
|
||||||
|
ct: CancellationToken,
|
||||||
) -> Response<BoxBody<Bytes, Infallible>> {
|
) -> Response<BoxBody<Bytes, Infallible>> {
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
let stream = SseBody::new(stream.map(|message| {
|
let stream = stream
|
||||||
let data = serde_json::to_string(&message.message).expect("valid message");
|
.map(|message| {
|
||||||
let mut sse = Sse::default().data(data);
|
let data = serde_json::to_string(&message.message).expect("valid message");
|
||||||
sse.id = message.event_id;
|
let mut sse = Sse::default().data(data);
|
||||||
Result::<Sse, Infallible>::Ok(sse)
|
sse.id = message.event_id;
|
||||||
}));
|
Result::<Sse, Infallible>::Ok(sse)
|
||||||
|
})
|
||||||
|
.take_until(async move { ct.cancelled().await });
|
||||||
|
let stream = SseBody::new(stream);
|
||||||
|
|
||||||
let stream = match keep_alive {
|
let stream = match keep_alive {
|
||||||
Some(duration) => stream
|
Some(duration) => stream
|
||||||
.with_keep_alive::<TokioTimer>(KeepAlive::new().interval(duration))
|
.with_keep_alive::<TokioTimer>(KeepAlive::new().interval(duration))
|
||||||
.boxed(),
|
.boxed(),
|
||||||
None => stream.boxed(),
|
None => stream.boxed(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(http::StatusCode::OK)
|
.status(http::StatusCode::OK)
|
||||||
.header(http::header::CONTENT_TYPE, EVENT_STREAM_MIME_TYPE)
|
.header(http::header::CONTENT_TYPE, EVENT_STREAM_MIME_TYPE)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use http::{Method, Request, Response, header::ALLOW};
|
||||||
use http_body::Body;
|
use http_body::Body;
|
||||||
use http_body_util::{BodyExt, Full, combinators::BoxBody};
|
use http_body_util::{BodyExt, Full, combinators::BoxBody};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use super::session::SessionManager;
|
use super::session::SessionManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -33,6 +34,11 @@ pub struct StreamableHttpServerConfig {
|
||||||
pub sse_keep_alive: Option<Duration>,
|
pub sse_keep_alive: Option<Duration>,
|
||||||
/// If true, the server will create a session for each request and keep it alive.
|
/// If true, the server will create a session for each request and keep it alive.
|
||||||
pub stateful_mode: bool,
|
pub stateful_mode: bool,
|
||||||
|
/// Cancellation token for the Streamable HTTP server.
|
||||||
|
///
|
||||||
|
/// When this token is cancelled, all active sessions are terminated and
|
||||||
|
/// the server stops accepting new requests.
|
||||||
|
pub cancellation_token: CancellationToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StreamableHttpServerConfig {
|
impl Default for StreamableHttpServerConfig {
|
||||||
|
|
@ -40,6 +46,7 @@ impl Default for StreamableHttpServerConfig {
|
||||||
Self {
|
Self {
|
||||||
sse_keep_alive: Some(Duration::from_secs(15)),
|
sse_keep_alive: Some(Duration::from_secs(15)),
|
||||||
stateful_mode: true,
|
stateful_mode: true,
|
||||||
|
cancellation_token: CancellationToken::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +216,11 @@ where
|
||||||
.resume(&session_id, last_event_id)
|
.resume(&session_id, last_event_id)
|
||||||
.await
|
.await
|
||||||
.map_err(internal_error_response("resume session"))?;
|
.map_err(internal_error_response("resume session"))?;
|
||||||
Ok(sse_stream_response(stream, self.config.sse_keep_alive))
|
Ok(sse_stream_response(
|
||||||
|
stream,
|
||||||
|
self.config.sse_keep_alive,
|
||||||
|
self.config.cancellation_token.child_token(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
// create standalone stream
|
// create standalone stream
|
||||||
let stream = self
|
let stream = self
|
||||||
|
|
@ -217,7 +228,11 @@ where
|
||||||
.create_standalone_stream(&session_id)
|
.create_standalone_stream(&session_id)
|
||||||
.await
|
.await
|
||||||
.map_err(internal_error_response("create standalone stream"))?;
|
.map_err(internal_error_response("create standalone stream"))?;
|
||||||
Ok(sse_stream_response(stream, self.config.sse_keep_alive))
|
Ok(sse_stream_response(
|
||||||
|
stream,
|
||||||
|
self.config.sse_keep_alive,
|
||||||
|
self.config.cancellation_token.child_token(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,7 +322,11 @@ where
|
||||||
.create_stream(&session_id, message)
|
.create_stream(&session_id, message)
|
||||||
.await
|
.await
|
||||||
.map_err(internal_error_response("get session"))?;
|
.map_err(internal_error_response("get session"))?;
|
||||||
Ok(sse_stream_response(stream, self.config.sse_keep_alive))
|
Ok(sse_stream_response(
|
||||||
|
stream,
|
||||||
|
self.config.sse_keep_alive,
|
||||||
|
self.config.cancellation_token.child_token(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
ClientJsonRpcMessage::Notification(_)
|
ClientJsonRpcMessage::Notification(_)
|
||||||
| ClientJsonRpcMessage::Response(_)
|
| ClientJsonRpcMessage::Response(_)
|
||||||
|
|
@ -380,6 +399,7 @@ where
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
self.config.sse_keep_alive,
|
self.config.sse_keep_alive,
|
||||||
|
self.config.cancellation_token.child_token(),
|
||||||
);
|
);
|
||||||
|
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
|
|
@ -413,6 +433,7 @@ where
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
self.config.sse_keep_alive,
|
self.config.sse_keep_alive,
|
||||||
|
self.config.cancellation_token.child_token(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ClientJsonRpcMessage::Notification(_notification) => {
|
ClientJsonRpcMessage::Notification(_notification) => {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ async fn test_with_js_streamable_http_client() -> anyhow::Result<()> {
|
||||||
.wait()
|
.wait()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let ct = CancellationToken::new();
|
||||||
let service: StreamableHttpService<Calculator, LocalSessionManager> =
|
let service: StreamableHttpService<Calculator, LocalSessionManager> =
|
||||||
StreamableHttpService::new(
|
StreamableHttpService::new(
|
||||||
|| Ok(Calculator::new()),
|
|| Ok(Calculator::new()),
|
||||||
|
|
@ -101,11 +102,12 @@ async fn test_with_js_streamable_http_client() -> anyhow::Result<()> {
|
||||||
StreamableHttpServerConfig {
|
StreamableHttpServerConfig {
|
||||||
stateful_mode: true,
|
stateful_mode: true,
|
||||||
sse_keep_alive: None,
|
sse_keep_alive: None,
|
||||||
|
cancellation_token: ct.child_token(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let router = axum::Router::new().nest_service("/mcp", service);
|
let router = axum::Router::new().nest_service("/mcp", service);
|
||||||
let tcp_listener = tokio::net::TcpListener::bind(STREAMABLE_HTTP_BIND_ADDRESS).await?;
|
let tcp_listener = tokio::net::TcpListener::bind(STREAMABLE_HTTP_BIND_ADDRESS).await?;
|
||||||
let ct = CancellationToken::new();
|
|
||||||
let handle = tokio::spawn({
|
let handle = tokio::spawn({
|
||||||
let ct = ct.clone();
|
let ct = ct.clone();
|
||||||
async move {
|
async move {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use rmcp::transport::streamable_http_server::{
|
use rmcp::transport::streamable_http_server::{
|
||||||
StreamableHttpService, session::local::LocalSessionManager,
|
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
|
||||||
};
|
};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
layer::SubscriberExt,
|
layer::SubscriberExt,
|
||||||
|
|
@ -20,17 +20,24 @@ async fn main() -> anyhow::Result<()> {
|
||||||
)
|
)
|
||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
let ct = tokio_util::sync::CancellationToken::new();
|
||||||
|
|
||||||
let service = StreamableHttpService::new(
|
let service = StreamableHttpService::new(
|
||||||
|| Ok(Counter::new()),
|
|| Ok(Counter::new()),
|
||||||
LocalSessionManager::default().into(),
|
LocalSessionManager::default().into(),
|
||||||
Default::default(),
|
StreamableHttpServerConfig {
|
||||||
|
cancellation_token: ct.child_token(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let router = axum::Router::new().nest_service("/mcp", service);
|
let router = axum::Router::new().nest_service("/mcp", service);
|
||||||
let tcp_listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?;
|
let tcp_listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?;
|
||||||
let _ = axum::serve(tcp_listener, router)
|
let _ = axum::serve(tcp_listener, router)
|
||||||
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() })
|
.with_graceful_shutdown(async move {
|
||||||
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
|
ct.cancel();
|
||||||
|
})
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let mut server = ServerOptions::new()
|
let mut server = ServerOptions::new()
|
||||||
.first_pipe_instance(true)
|
.first_pipe_instance(true)
|
||||||
.create(name)?;
|
.create(name)?;
|
||||||
while let Ok(_) = server.connect().await {
|
while server.connect().await.is_ok() {
|
||||||
let stream = server;
|
let stream = server;
|
||||||
server = ServerOptions::new().create(name)?;
|
server = ServerOptions::new().create(name)?;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match serve_server(Calculator, stream).await {
|
match serve_server(Calculator::new(), stream).await {
|
||||||
Ok(server) => {
|
Ok(server) => {
|
||||||
println!("Server initialized successfully");
|
println!("Server initialized successfully");
|
||||||
if let Err(e) = server.waiting().await {
|
if let Err(e) = server.waiting().await {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue