1use core::fmt;
31use std::collections::HashMap;
32use std::sync::Arc;
33use std::time::Duration;
34
35use crate::domain::RequestId;
36
37use super::cancellation::{AbortReason, CancelReason, CancellationToken};
38use super::progress::{ProgressCursor, ProgressToken, ProgressUpdate};
39use super::state::Instant;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum RequestOrigin {
50 Inbound,
52 Outbound,
54}
55
56#[derive(Debug)]
58pub struct InFlight {
59 origin: RequestOrigin,
60 method: Arc<str>,
61 token: Option<ProgressToken>,
62 cursor: ProgressCursor,
63 budget: Option<Duration>,
64 started_at: Instant,
65 cancel: CancellationToken,
66}
67
68impl InFlight {
69 #[must_use]
76 pub fn new(
77 origin: RequestOrigin,
78 method: impl AsRef<str>,
79 token: Option<ProgressToken>,
80 budget: Option<Duration>,
81 started_at: Instant,
82 ) -> Self {
83 Self {
84 origin,
85 method: Arc::from(method.as_ref()),
86 token,
87 cursor: ProgressCursor::new(),
88 budget,
89 started_at,
90 cancel: CancellationToken::new(),
91 }
92 }
93
94 #[must_use]
96 pub const fn origin(&self) -> RequestOrigin {
97 self.origin
98 }
99
100 #[must_use]
102 pub fn method(&self) -> &str {
103 &self.method
104 }
105
106 #[must_use]
108 pub const fn progress_token(&self) -> Option<&ProgressToken> {
109 self.token.as_ref()
110 }
111
112 #[must_use]
114 pub const fn budget(&self) -> Option<Duration> {
115 self.budget
116 }
117
118 #[must_use]
120 pub const fn started_at(&self) -> Instant {
121 self.started_at
122 }
123
124 #[must_use]
126 pub const fn cancellation(&self) -> &CancellationToken {
127 &self.cancel
128 }
129
130 #[must_use]
136 pub fn cancellation_handle(&self) -> CancellationToken {
137 self.cancel.clone()
138 }
139
140 #[must_use]
142 pub fn has_expired(&self, now: Instant) -> bool {
143 self.budget
144 .is_some_and(|budget| now.since(self.started_at) >= budget)
145 }
146
147 #[must_use]
154 pub fn advance_progress(&mut self, progress: f64, message: Option<String>) -> bool {
155 let Some(token) = self.token.clone() else {
156 return false;
157 };
158 let mut update = ProgressUpdate::new(token, progress);
159 if let Some(message) = message {
160 update = update.with_message(message);
161 }
162 self.cursor.advance(&update)
163 }
164
165 #[must_use]
167 pub const fn furthest_progress(&self) -> Option<f64> {
168 self.cursor.furthest()
169 }
170
171 pub fn abort(&self, reason: AbortReason) -> bool {
175 self.cancel.cancel(reason)
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum CancelOutcome {
182 Cancelled,
184 Ignored,
191}
192
193impl CancelOutcome {
194 #[must_use]
196 pub const fn cancelled(self) -> bool {
197 matches!(self, Self::Cancelled)
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct DuplicateRequest {
208 pub id: RequestId,
210}
211
212impl fmt::Display for DuplicateRequest {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(
215 formatter,
216 "request id {} is already in flight",
217 self.id.redacted()
218 )
219 }
220}
221
222impl std::error::Error for DuplicateRequest {}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct UnknownRequest {
230 pub id: RequestId,
232}
233
234impl fmt::Display for UnknownRequest {
235 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
236 write!(
237 formatter,
238 "request id {} is not in flight",
239 self.id.redacted()
240 )
241 }
242}
243
244impl std::error::Error for UnknownRequest {}
245
246#[derive(Debug, Default)]
248pub struct RequestRegistry {
249 entries: HashMap<RequestId, InFlight>,
250}
251
252impl RequestRegistry {
253 #[must_use]
255 pub fn new() -> Self {
256 Self {
257 entries: HashMap::new(),
258 }
259 }
260
261 pub fn register(&mut self, id: RequestId, entry: InFlight) -> Result<(), DuplicateRequest> {
269 if self.entries.contains_key(&id) {
270 return Err(DuplicateRequest { id });
271 }
272 self.entries.insert(id, entry);
273 Ok(())
274 }
275
276 #[must_use]
278 pub fn get(&self, id: &RequestId) -> Option<&InFlight> {
279 self.entries.get(id)
280 }
281
282 #[must_use]
284 pub fn get_mut(&mut self, id: &RequestId) -> Option<&mut InFlight> {
285 self.entries.get_mut(id)
286 }
287
288 pub fn cancel(&mut self, id: &RequestId, reason: Option<CancelReason>) -> CancelOutcome {
292 let Some(entry) = self.entries.get(id) else {
293 return CancelOutcome::Ignored;
294 };
295 if entry.abort(AbortReason::Cancelled(reason)) {
296 CancelOutcome::Cancelled
297 } else {
298 CancelOutcome::Ignored
302 }
303 }
304
305 pub fn expire(&mut self, now: Instant) -> Vec<RequestId> {
311 let mut expired = Vec::new();
312 self.entries.retain(|id, entry| {
313 if entry.has_expired(now) {
314 entry.abort(AbortReason::Expired);
315 expired.push(id.clone());
316 false
317 } else {
318 true
319 }
320 });
321 expired
322 }
323
324 #[must_use]
326 pub fn complete(&mut self, id: &RequestId) -> Option<InFlight> {
327 self.entries.remove(id)
328 }
329
330 pub fn ids(&self) -> impl Iterator<Item = &RequestId> {
332 self.entries.keys()
333 }
334
335 #[must_use]
337 pub fn len(&self) -> usize {
338 self.entries.len()
339 }
340
341 #[must_use]
343 pub fn is_empty(&self) -> bool {
344 self.entries.is_empty()
345 }
346
347 pub fn drain(&mut self) -> Vec<RequestId> {
353 let mut ids = Vec::with_capacity(self.entries.len());
354 for (id, entry) in &self.entries {
355 entry.abort(AbortReason::SessionClosed);
356 ids.push(id.clone());
357 }
358 self.entries.clear();
359 ids
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use std::time::Duration;
366
367 use super::{
368 CancelOutcome, DuplicateRequest, InFlight, RequestOrigin, RequestRegistry, UnknownRequest,
369 };
370 use crate::domain::RequestId;
371 use crate::domain::session::cancellation::{AbortReason, CancelReason, CancellationToken};
372 use crate::domain::session::progress::ProgressToken;
373 use crate::domain::session::state::Instant;
374
375 #[derive(Debug)]
383 #[expect(dead_code, reason = "the payloads are read by the derived Debug")]
384 enum Failure {
385 Duplicate(DuplicateRequest),
387 Missing(&'static str),
389 }
390
391 impl From<DuplicateRequest> for Failure {
392 fn from(duplicate: DuplicateRequest) -> Self {
393 Self::Duplicate(duplicate)
394 }
395 }
396
397 fn at(millis: u64) -> Instant {
398 Instant::from_duration(Duration::from_millis(millis))
399 }
400
401 fn entry(token: Option<ProgressToken>, budget: Option<Duration>) -> InFlight {
402 InFlight::new(RequestOrigin::Inbound, "tools/call", token, budget, at(0))
403 }
404
405 fn handle(registry: &RequestRegistry, id: &RequestId) -> Result<CancellationToken, Failure> {
407 registry
408 .get(id)
409 .map(InFlight::cancellation_handle)
410 .ok_or(Failure::Missing("the request that was just registered"))
411 }
412
413 #[test]
414 fn a_request_is_found_until_it_completes() -> Result<(), Failure> {
415 let mut registry = RequestRegistry::new();
416 let id = RequestId::number(1);
417 registry.register(id.clone(), entry(None, None))?;
418 assert_eq!(registry.len(), 1);
419 assert_eq!(registry.get(&id).map(InFlight::method), Some("tools/call"));
420
421 assert!(registry.complete(&id).is_some());
422 assert!(registry.is_empty());
423 assert!(registry.complete(&id).is_none());
425 Ok(())
426 }
427
428 #[test]
429 fn a_duplicate_in_flight_id_is_refused_and_the_first_entry_survives() -> Result<(), Failure> {
430 let mut registry = RequestRegistry::new();
431 let id = RequestId::number(1);
432 registry.register(id.clone(), entry(None, None))?;
433
434 let refused = registry.register(
435 id.clone(),
436 InFlight::new(RequestOrigin::Inbound, "resources/read", None, None, at(0)),
437 );
438 assert_matches::assert_matches!(
439 refused,
440 Err(DuplicateRequest { id: ref reported }) if *reported == RequestId::number(1)
441 );
442 assert_eq!(registry.get(&id).map(InFlight::method), Some("tools/call"));
445 Ok(())
446 }
447
448 #[test]
451 fn cancelling_a_finished_or_unknown_request_is_a_no_op() -> Result<(), Failure> {
452 let mut registry = RequestRegistry::new();
453 let id = RequestId::number(7);
454 registry.register(id.clone(), entry(None, None))?;
455 assert!(
456 registry.complete(&id).is_some(),
457 "the entry was registered a line ago"
458 );
459
460 assert_eq!(registry.cancel(&id, None), CancelOutcome::Ignored);
461 assert_eq!(
462 registry.cancel(&RequestId::number(99), None),
463 CancelOutcome::Ignored
464 );
465 Ok(())
466 }
467
468 #[test]
469 fn cancelling_twice_reports_only_the_first() -> Result<(), Failure> {
470 let mut registry = RequestRegistry::new();
471 let id = RequestId::number(1);
472 registry.register(id.clone(), entry(None, None))?;
473
474 let first = registry.cancel(&id, Some(CancelReason::new("user")));
475 let second = registry.cancel(&id, Some(CancelReason::new("timeout")));
476
477 assert_eq!(first, CancelOutcome::Cancelled);
478 assert_eq!(second, CancelOutcome::Ignored);
479 assert_eq!(
482 registry
483 .get(&id)
484 .and_then(|entry| entry.cancellation().reason()),
485 Some(&AbortReason::Cancelled(Some(CancelReason::new("user"))))
486 );
487 Ok(())
488 }
489
490 #[test]
491 fn a_zero_budget_expires_immediately_and_an_absent_budget_never_expires() -> Result<(), Failure>
492 {
493 let mut registry = RequestRegistry::new();
494 let bounded = RequestId::number(1);
495 let unbounded = RequestId::number(2);
496 registry.register(
497 bounded.clone(),
498 entry(None, Some(Duration::from_millis(100))),
499 )?;
500 registry.register(unbounded.clone(), entry(None, None))?;
501
502 assert!(
503 registry.expire(at(99)).is_empty(),
504 "expired a millisecond early"
505 );
506 assert_eq!(registry.expire(at(100)), vec![bounded]);
507 assert_eq!(registry.len(), 1);
508 assert!(registry.expire(at(1_000_000)).is_empty());
510 assert!(registry.get(&unbounded).is_some());
511 Ok(())
512 }
513
514 #[test]
515 fn an_expired_request_is_aborted_so_its_handler_wakes() -> Result<(), Failure> {
516 let mut registry = RequestRegistry::new();
517 let id = RequestId::number(1);
518 registry.register(id.clone(), entry(None, Some(Duration::from_secs(1))))?;
519 let token = handle(®istry, &id)?;
520
521 registry.expire(at(1_001));
522 assert!(token.is_cancelled());
523 assert_eq!(token.reason(), Some(&AbortReason::Expired));
524 Ok(())
525 }
526
527 #[test]
528 fn progress_needs_a_token_and_moves_forward_only() -> Result<(), Failure> {
529 let mut registry = RequestRegistry::new();
530 let without = RequestId::number(1);
531 let with = RequestId::number(2);
532 registry.register(without.clone(), entry(None, None))?;
533 registry.register(with.clone(), entry(Some(ProgressToken::text("job")), None))?;
534
535 let advanced = registry
537 .get_mut(&without)
538 .is_some_and(|entry| entry.advance_progress(1.0, None));
539 assert!(!advanced, "a request with no progress token never emits");
540
541 let entry = registry
542 .get_mut(&with)
543 .ok_or(Failure::Missing("the request carrying a progress token"))?;
544 assert!(entry.advance_progress(1.0, Some(String::from("reading"))));
545 assert!(!entry.advance_progress(1.0, None));
546 assert!(entry.advance_progress(2.0, None));
547 assert_eq!(entry.furthest_progress(), Some(2.0));
548 assert_eq!(entry.progress_token(), Some(&ProgressToken::text("job")));
549 Ok(())
550 }
551
552 #[test]
553 fn draining_aborts_everything_it_forgets() -> Result<(), Failure> {
554 let mut registry = RequestRegistry::new();
555 for n in 0..3 {
556 registry.register(RequestId::number(n), entry(None, None))?;
557 }
558 let tokens: Vec<_> = registry
559 .ids()
560 .map(|id| registry.get(id).map(InFlight::cancellation_handle))
561 .collect();
562
563 let drained = registry.drain();
564 assert_eq!(drained.len(), 3);
565 assert!(registry.is_empty());
566 assert_eq!(
567 tokens.iter().filter(|token| token.is_none()).count(),
568 0,
569 "every registered id had an entry before the drain"
570 );
571 for token in tokens.into_iter().flatten() {
572 assert_eq!(token.reason(), Some(&AbortReason::SessionClosed));
573 }
574 Ok(())
575 }
576
577 #[test]
580 fn an_error_message_redacts_the_id() {
581 let secret = RequestId::text("super-secret-credential");
582 let message = DuplicateRequest { id: secret.clone() }.to_string();
583 assert!(!message.contains("super-secret-credential"), "{message}");
584 assert!(message.contains(&secret.redacted()), "{message}");
585
586 let unknown = UnknownRequest { id: secret }.to_string();
587 assert!(!unknown.contains("super-secret-credential"), "{unknown}");
588 }
589
590 #[test]
591 fn both_origins_share_one_table() -> Result<(), Failure> {
592 assert_ne!(RequestOrigin::Inbound, RequestOrigin::Outbound);
593 let mut registry = RequestRegistry::new();
594 registry.register(
595 RequestId::number(1),
596 InFlight::new(
597 RequestOrigin::Outbound,
598 "sampling/createMessage",
599 None,
600 None,
601 at(0),
602 ),
603 )?;
604 assert_eq!(
605 registry.get(&RequestId::number(1)).map(InFlight::origin),
606 Some(RequestOrigin::Outbound)
607 );
608 Ok(())
609 }
610}