Skip to content

Commit 5d00e20

Browse files
Add progress-aware request timeout reset (#858)
* feat: add progress-aware request timeouts * Update crates/rmcp/src/service.rs Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com> * refactor(rmcp): move helpers and simplify response waiting --------- Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
1 parent 4b82e41 commit 5d00e20

4 files changed

Lines changed: 441 additions & 21 deletions

File tree

crates/rmcp/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ name = "test_progress_subscriber"
252252
required-features = ["server", "client", "macros"]
253253
path = "tests/test_progress_subscriber.rs"
254254

255+
[[test]]
256+
name = "test_request_timeout_progress"
257+
required-features = ["server", "client", "macros"]
258+
path = "tests/test_request_timeout_progress.rs"
259+
255260
[[test]]
256261
name = "test_elicitation"
257262
required-features = ["elicitation", "client", "server"]

crates/rmcp/src/service.rs

Lines changed: 229 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,12 @@ pub(crate) type MaybeBoxFuture<'a, T> = BoxFuture<'a, T>;
4242
#[cfg(feature = "local")]
4343
pub(crate) type MaybeBoxFuture<'a, T> = LocalBoxFuture<'a, T>;
4444

45+
#[cfg(feature = "server")]
46+
use crate::model::ClientNotification;
4547
#[cfg(feature = "server")]
4648
use crate::model::ServerJsonRpcMessage;
49+
#[cfg(feature = "client")]
50+
use crate::model::ServerNotification;
4751
use crate::{
4852
error::ErrorData as McpError,
4953
model::{
@@ -299,7 +303,37 @@ impl ProgressTokenProvider for AtomicU32Provider {
299303
}
300304
}
301305

306+
#[doc(hidden)]
307+
pub trait ProgressNotificationToken {
308+
fn progress_token(&self) -> Option<&ProgressToken>;
309+
}
310+
311+
#[cfg(feature = "server")]
312+
impl ProgressNotificationToken for ClientNotification {
313+
fn progress_token(&self) -> Option<&ProgressToken> {
314+
match self {
315+
ClientNotification::ProgressNotification(notification) => {
316+
Some(&notification.params.progress_token)
317+
}
318+
_ => None,
319+
}
320+
}
321+
}
322+
323+
#[cfg(feature = "client")]
324+
impl ProgressNotificationToken for ServerNotification {
325+
fn progress_token(&self) -> Option<&ProgressToken> {
326+
match self {
327+
ServerNotification::ProgressNotification(notification) => {
328+
Some(&notification.params.progress_token)
329+
}
330+
_ => None,
331+
}
332+
}
333+
}
334+
302335
type Responder<T> = tokio::sync::oneshot::Sender<T>;
336+
type ProgressTimeoutWatchers = Arc<tokio::sync::RwLock<HashMap<ProgressToken, mpsc::Sender<()>>>>;
303337

304338
/// A handle to a remote request
305339
///
@@ -314,40 +348,126 @@ pub struct RequestHandle<R: ServiceRole> {
314348
pub peer: Peer<R>,
315349
pub id: RequestId,
316350
pub progress_token: ProgressToken,
351+
progress_timeout_watchers: ProgressTimeoutWatchers,
352+
progress_reset_rx: Option<mpsc::Receiver<()>>,
317353
}
318354

319355
impl<R: ServiceRole> RequestHandle<R> {
320356
pub const REQUEST_TIMEOUT_REASON: &str = "request timeout";
321-
pub async fn await_response(self) -> Result<R::PeerResp, ServiceError> {
322-
if let Some(timeout) = self.options.timeout {
323-
let timeout_result = tokio::time::timeout(timeout, async move {
324-
self.rx.await.map_err(|_e| ServiceError::TransportClosed)?
325-
})
326-
.await;
327-
match timeout_result {
328-
Ok(response) => response,
357+
pub const REQUEST_MAX_TOTAL_TIMEOUT_REASON: &str = "maximum total timeout exceeded";
358+
359+
pub async fn await_response(mut self) -> Result<R::PeerResp, ServiceError> {
360+
let timeout = self.options.timeout;
361+
let max_total_timeout = self.options.max_total_timeout;
362+
let reset_timeout_on_progress = self.options.reset_timeout_on_progress;
363+
364+
let has_progress_reset_rx = self.progress_reset_rx.is_some();
365+
let progress_token = self.progress_token.clone();
366+
367+
let result = match (timeout, max_total_timeout, reset_timeout_on_progress) {
368+
(Some(timeout), None, false) => match tokio::time::timeout(timeout, &mut self.rx).await
369+
{
370+
Ok(response) => response.map_err(|_e| ServiceError::TransportClosed)?,
329371
Err(_) => {
330372
let error = Err(ServiceError::Timeout { timeout });
331373
// cancel this request
332-
let notification = CancelledNotification {
333-
params: CancelledNotificationParam {
334-
request_id: self.id,
335-
reason: Some(Self::REQUEST_TIMEOUT_REASON.to_owned()),
336-
},
337-
method: crate::model::CancelledNotificationMethod,
338-
extensions: Default::default(),
339-
};
340-
let _ = self.peer.send_notification(notification.into()).await;
374+
self.send_timeout_cancel_notification(Self::REQUEST_TIMEOUT_REASON)
375+
.await;
341376
error
342377
}
378+
},
379+
(None, None, _) => (&mut self.rx)
380+
.await
381+
.map_err(|_e| ServiceError::TransportClosed)?,
382+
_ => {
383+
self.await_response_with_progress_timeout(
384+
timeout,
385+
max_total_timeout,
386+
reset_timeout_on_progress,
387+
)
388+
.await
389+
}
390+
};
391+
392+
Self::cleanup_progress_timeout_watcher(
393+
&self.peer.progress_timeout_watchers,
394+
&progress_token,
395+
has_progress_reset_rx,
396+
)
397+
.await;
398+
result
399+
}
400+
401+
async fn send_timeout_cancel_notification(&self, reason: &str) {
402+
let notification = CancelledNotification {
403+
params: CancelledNotificationParam {
404+
request_id: self.id.clone(),
405+
reason: Some(reason.to_owned()),
406+
},
407+
method: crate::model::CancelledNotificationMethod,
408+
extensions: Default::default(),
409+
};
410+
let _ = self.peer.send_notification(notification.into()).await;
411+
}
412+
413+
async fn await_response_with_progress_timeout(
414+
&mut self,
415+
timeout: Option<Duration>,
416+
max_total_timeout: Option<Duration>,
417+
reset_timeout_on_progress: bool,
418+
) -> Result<R::PeerResp, ServiceError> {
419+
let mut idle_sleep = timeout.map(tokio::time::sleep).map(Box::pin);
420+
let mut max_total_sleep = max_total_timeout.map(tokio::time::sleep).map(Box::pin);
421+
422+
loop {
423+
tokio::select! {
424+
biased;
425+
426+
response = &mut self.rx => {
427+
return response.map_err(|_e| ServiceError::TransportClosed)?;
428+
}
429+
_ = async {
430+
if let Some(sleep) = idle_sleep.as_mut() {
431+
sleep.as_mut().await;
432+
}
433+
}, if idle_sleep.is_some() => {
434+
let timeout = timeout.expect("idle timeout exists when idle sleep exists");
435+
self.send_timeout_cancel_notification(Self::REQUEST_TIMEOUT_REASON).await;
436+
return Err(ServiceError::Timeout { timeout });
437+
}
438+
_ = async {
439+
if let Some(sleep) = max_total_sleep.as_mut() {
440+
sleep.as_mut().await;
441+
}
442+
}, if max_total_sleep.is_some() => {
443+
let timeout = max_total_timeout.expect("max total timeout exists when max total sleep exists");
444+
self.send_timeout_cancel_notification(Self::REQUEST_MAX_TOTAL_TIMEOUT_REASON).await;
445+
return Err(ServiceError::Timeout { timeout });
446+
}
447+
progress = async {
448+
match self.progress_reset_rx.as_mut() {
449+
Some(rx) => rx.recv().await,
450+
None => None,
451+
}
452+
}, if reset_timeout_on_progress && timeout.is_some() && self.progress_reset_rx.is_some() => {
453+
if progress.is_some() {
454+
if let (Some(timeout), Some(sleep)) = (timeout, idle_sleep.as_mut()) {
455+
sleep.as_mut().reset(tokio::time::Instant::now() + timeout);
456+
}
457+
}
458+
}
343459
}
344-
} else {
345-
self.rx.await.map_err(|_e| ServiceError::TransportClosed)?
346460
}
347461
}
348462

349463
/// Cancel this request
350464
pub async fn cancel(self, reason: Option<String>) -> Result<(), ServiceError> {
465+
Self::cleanup_progress_timeout_watcher(
466+
&self.progress_timeout_watchers,
467+
&self.progress_token,
468+
self.progress_reset_rx.is_some(),
469+
)
470+
.await;
351471
let notification = CancelledNotification {
352472
params: CancelledNotificationParam {
353473
request_id: self.id,
@@ -359,6 +479,19 @@ impl<R: ServiceRole> RequestHandle<R> {
359479
self.peer.send_notification(notification.into()).await?;
360480
Ok(())
361481
}
482+
483+
async fn cleanup_progress_timeout_watcher(
484+
progress_timeout_watchers: &ProgressTimeoutWatchers,
485+
progress_token: &ProgressToken,
486+
has_progress_reset_rx: bool,
487+
) {
488+
if has_progress_reset_rx {
489+
progress_timeout_watchers
490+
.write()
491+
.await
492+
.remove(progress_token);
493+
}
494+
}
362495
}
363496

364497
#[derive(Debug)]
@@ -384,6 +517,7 @@ pub struct Peer<R: ServiceRole> {
384517
tx: mpsc::Sender<PeerSinkMessage<R>>,
385518
request_id_provider: Arc<dyn RequestIdProvider>,
386519
progress_token_provider: Arc<dyn ProgressTokenProvider>,
520+
progress_timeout_watchers: ProgressTimeoutWatchers,
387521
info: Arc<std::sync::RwLock<Option<Arc<R::PeerInfo>>>>,
388522
}
389523

@@ -403,12 +537,33 @@ type ProxyOutbound<R> = mpsc::Receiver<PeerSinkMessage<R>>;
403537
pub struct PeerRequestOptions {
404538
pub timeout: Option<Duration>,
405539
pub meta: Option<Meta>,
540+
/// Reset the request timeout when a matching progress notification is received.
541+
pub reset_timeout_on_progress: bool,
542+
/// Maximum total time to wait for the request, regardless of progress notifications.
543+
pub max_total_timeout: Option<Duration>,
406544
}
407545

408546
impl PeerRequestOptions {
409547
pub fn no_options() -> Self {
410548
Self::default()
411549
}
550+
551+
pub fn with_timeout(timeout: Duration) -> Self {
552+
Self {
553+
timeout: Some(timeout),
554+
..Self::default()
555+
}
556+
}
557+
558+
pub fn reset_timeout_on_progress(mut self) -> Self {
559+
self.reset_timeout_on_progress = true;
560+
self
561+
}
562+
563+
pub fn with_max_total_timeout(mut self, timeout: Duration) -> Self {
564+
self.max_total_timeout = Some(timeout);
565+
self
566+
}
412567
}
413568

414569
impl<R: ServiceRole> Peer<R> {
@@ -423,6 +578,7 @@ impl<R: ServiceRole> Peer<R> {
423578
tx,
424579
request_id_provider,
425580
progress_token_provider: Arc::new(AtomicU32ProgressTokenProvider::default()),
581+
progress_timeout_watchers: Default::default(),
426582
info: Arc::new(std::sync::RwLock::new(peer_info.map(Arc::new))),
427583
},
428584
rx,
@@ -468,22 +624,68 @@ impl<R: ServiceRole> Peer<R> {
468624
request.get_meta_mut().extend(meta);
469625
}
470626
let (responder, receiver) = tokio::sync::oneshot::channel();
471-
self.tx
627+
let progress_reset_rx = if options.reset_timeout_on_progress && options.timeout.is_some() {
628+
let (sender, receiver) = mpsc::channel(1);
629+
self.progress_timeout_watchers
630+
.write()
631+
.await
632+
.insert(progress_token.clone(), sender);
633+
Some(receiver)
634+
} else {
635+
None
636+
};
637+
if self
638+
.tx
472639
.send(PeerSinkMessage::Request {
473640
request,
474641
id: id.clone(),
475642
responder,
476643
})
477644
.await
478-
.map_err(|_m| ServiceError::TransportClosed)?;
645+
.is_err()
646+
{
647+
if progress_reset_rx.is_some() {
648+
self.progress_timeout_watchers
649+
.write()
650+
.await
651+
.remove(&progress_token);
652+
}
653+
return Err(ServiceError::TransportClosed);
654+
}
479655
Ok(RequestHandle {
480656
id,
481657
rx: receiver,
482658
progress_token,
483659
options,
484660
peer: self.clone(),
661+
progress_timeout_watchers: self.progress_timeout_watchers.clone(),
662+
progress_reset_rx,
485663
})
486664
}
665+
666+
async fn notify_progress_timeout_watcher(&self, progress_token: &ProgressToken) {
667+
let sender = self
668+
.progress_timeout_watchers
669+
.read()
670+
.await
671+
.get(progress_token)
672+
.cloned();
673+
if let Some(sender) = sender {
674+
match sender.try_send(()) {
675+
Ok(()) => {}
676+
Err(mpsc::error::TrySendError::Full(_)) => {
677+
tracing::trace!(?progress_token, "progress timeout watcher channel is full");
678+
}
679+
Err(mpsc::error::TrySendError::Closed(_)) => {
680+
self.progress_timeout_watchers
681+
.write()
682+
.await
683+
.remove(progress_token);
684+
}
685+
}
686+
}
687+
}
688+
487689
/// Snapshot of the peer's handshake info.
488690
pub fn peer_info(&self) -> Option<Arc<R::PeerInfo>> {
489691
self.info.read().expect("peer info lock poisoned").clone()
@@ -700,6 +902,7 @@ pub fn serve_directly<R, S, T, E, A>(
700902
) -> RunningService<R, S>
701903
where
702904
R: ServiceRole,
905+
R::PeerNot: ProgressNotificationToken,
703906
S: Service<R>,
704907
T: IntoTransport<R, E, A>,
705908
E: std::error::Error + Send + Sync + 'static,
@@ -716,6 +919,7 @@ pub fn serve_directly_with_ct<R, S, T, E, A>(
716919
) -> RunningService<R, S>
717920
where
718921
R: ServiceRole,
922+
R::PeerNot: ProgressNotificationToken,
719923
S: Service<R>,
720924
T: IntoTransport<R, E, A>,
721925
E: std::error::Error + Send + Sync + 'static,
@@ -756,6 +960,7 @@ fn serve_inner<R, S, T>(
756960
) -> RunningService<R, S>
757961
where
758962
R: ServiceRole,
963+
R::PeerNot: ProgressNotificationToken,
759964
S: Service<R>,
760965
T: Transport<R> + 'static,
761966
{
@@ -1002,6 +1207,9 @@ where
10021207
}
10031208
Err(notification) => notification,
10041209
};
1210+
if let Some(progress_token) = notification.progress_token() {
1211+
peer.notify_progress_timeout_watcher(progress_token).await;
1212+
}
10051213
{
10061214
let service = shared_service.clone();
10071215
let mut extensions = Extensions::new();

0 commit comments

Comments
 (0)