use core::fmt;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use crate::domain::RequestId;
use super::cancellation::{AbortReason, CancelReason, CancellationToken};
use super::progress::{ProgressCursor, ProgressToken, ProgressUpdate};
use super::state::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequestOrigin {
Inbound,
Outbound,
}
#[derive(Debug)]
pub struct InFlight {
origin: RequestOrigin,
method: Arc<str>,
token: Option<ProgressToken>,
cursor: ProgressCursor,
budget: Option<Duration>,
started_at: Instant,
cancel: CancellationToken,
}
impl InFlight {
#[must_use]
pub fn new(
origin: RequestOrigin,
method: impl AsRef<str>,
token: Option<ProgressToken>,
budget: Option<Duration>,
started_at: Instant,
) -> Self {
Self {
origin,
method: Arc::from(method.as_ref()),
token,
cursor: ProgressCursor::new(),
budget,
started_at,
cancel: CancellationToken::new(),
}
}
#[must_use]
pub const fn origin(&self) -> RequestOrigin {
self.origin
}
#[must_use]
pub fn method(&self) -> &str {
&self.method
}
#[must_use]
pub const fn progress_token(&self) -> Option<&ProgressToken> {
self.token.as_ref()
}
#[must_use]
pub const fn budget(&self) -> Option<Duration> {
self.budget
}
#[must_use]
pub const fn started_at(&self) -> Instant {
self.started_at
}
#[must_use]
pub const fn cancellation(&self) -> &CancellationToken {
&self.cancel
}
#[must_use]
pub fn cancellation_handle(&self) -> CancellationToken {
self.cancel.clone()
}
#[must_use]
pub fn has_expired(&self, now: Instant) -> bool {
self.budget
.is_some_and(|budget| now.since(self.started_at) >= budget)
}
#[must_use]
pub fn advance_progress(&mut self, progress: f64, message: Option<String>) -> bool {
let Some(token) = self.token.clone() else {
return false;
};
let mut update = ProgressUpdate::new(token, progress);
if let Some(message) = message {
update = update.with_message(message);
}
self.cursor.advance(&update)
}
#[must_use]
pub const fn furthest_progress(&self) -> Option<f64> {
self.cursor.furthest()
}
pub fn abort(&self, reason: AbortReason) -> bool {
self.cancel.cancel(reason)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CancelOutcome {
Cancelled,
Ignored,
}
impl CancelOutcome {
#[must_use]
pub const fn cancelled(self) -> bool {
matches!(self, Self::Cancelled)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateRequest {
pub id: RequestId,
}
impl fmt::Display for DuplicateRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"request id {} is already in flight",
self.id.redacted()
)
}
}
impl std::error::Error for DuplicateRequest {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownRequest {
pub id: RequestId,
}
impl fmt::Display for UnknownRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"request id {} is not in flight",
self.id.redacted()
)
}
}
impl std::error::Error for UnknownRequest {}
#[derive(Debug, Default)]
pub struct RequestRegistry {
entries: HashMap<RequestId, InFlight>,
}
impl RequestRegistry {
#[must_use]
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn register(&mut self, id: RequestId, entry: InFlight) -> Result<(), DuplicateRequest> {
if self.entries.contains_key(&id) {
return Err(DuplicateRequest { id });
}
self.entries.insert(id, entry);
Ok(())
}
#[must_use]
pub fn get(&self, id: &RequestId) -> Option<&InFlight> {
self.entries.get(id)
}
#[must_use]
pub fn get_mut(&mut self, id: &RequestId) -> Option<&mut InFlight> {
self.entries.get_mut(id)
}
pub fn cancel(&mut self, id: &RequestId, reason: Option<CancelReason>) -> CancelOutcome {
let Some(entry) = self.entries.get(id) else {
return CancelOutcome::Ignored;
};
if entry.abort(AbortReason::Cancelled(reason)) {
CancelOutcome::Cancelled
} else {
CancelOutcome::Ignored
}
}
pub fn expire(&mut self, now: Instant) -> Vec<RequestId> {
let mut expired = Vec::new();
self.entries.retain(|id, entry| {
if entry.has_expired(now) {
entry.abort(AbortReason::Expired);
expired.push(id.clone());
false
} else {
true
}
});
expired
}
#[must_use]
pub fn complete(&mut self, id: &RequestId) -> Option<InFlight> {
self.entries.remove(id)
}
pub fn ids(&self) -> impl Iterator<Item = &RequestId> {
self.entries.keys()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn drain(&mut self) -> Vec<RequestId> {
let mut ids = Vec::with_capacity(self.entries.len());
for (id, entry) in &self.entries {
entry.abort(AbortReason::SessionClosed);
ids.push(id.clone());
}
self.entries.clear();
ids
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{
CancelOutcome, DuplicateRequest, InFlight, RequestOrigin, RequestRegistry, UnknownRequest,
};
use crate::domain::RequestId;
use crate::domain::session::cancellation::{AbortReason, CancelReason, CancellationToken};
use crate::domain::session::progress::ProgressToken;
use crate::domain::session::state::Instant;
#[derive(Debug)]
#[expect(dead_code, reason = "the payloads are read by the derived Debug")]
enum Failure {
Duplicate(DuplicateRequest),
Missing(&'static str),
}
impl From<DuplicateRequest> for Failure {
fn from(duplicate: DuplicateRequest) -> Self {
Self::Duplicate(duplicate)
}
}
fn at(millis: u64) -> Instant {
Instant::from_duration(Duration::from_millis(millis))
}
fn entry(token: Option<ProgressToken>, budget: Option<Duration>) -> InFlight {
InFlight::new(RequestOrigin::Inbound, "tools/call", token, budget, at(0))
}
fn handle(registry: &RequestRegistry, id: &RequestId) -> Result<CancellationToken, Failure> {
registry
.get(id)
.map(InFlight::cancellation_handle)
.ok_or(Failure::Missing("the request that was just registered"))
}
#[test]
fn a_request_is_found_until_it_completes() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let id = RequestId::number(1);
registry.register(id.clone(), entry(None, None))?;
assert_eq!(registry.len(), 1);
assert_eq!(registry.get(&id).map(InFlight::method), Some("tools/call"));
assert!(registry.complete(&id).is_some());
assert!(registry.is_empty());
assert!(registry.complete(&id).is_none());
Ok(())
}
#[test]
fn a_duplicate_in_flight_id_is_refused_and_the_first_entry_survives() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let id = RequestId::number(1);
registry.register(id.clone(), entry(None, None))?;
let refused = registry.register(
id.clone(),
InFlight::new(RequestOrigin::Inbound, "resources/read", None, None, at(0)),
);
assert_matches::assert_matches!(
refused,
Err(DuplicateRequest { id: ref reported }) if *reported == RequestId::number(1)
);
assert_eq!(registry.get(&id).map(InFlight::method), Some("tools/call"));
Ok(())
}
#[test]
fn cancelling_a_finished_or_unknown_request_is_a_no_op() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let id = RequestId::number(7);
registry.register(id.clone(), entry(None, None))?;
assert!(
registry.complete(&id).is_some(),
"the entry was registered a line ago"
);
assert_eq!(registry.cancel(&id, None), CancelOutcome::Ignored);
assert_eq!(
registry.cancel(&RequestId::number(99), None),
CancelOutcome::Ignored
);
Ok(())
}
#[test]
fn cancelling_twice_reports_only_the_first() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let id = RequestId::number(1);
registry.register(id.clone(), entry(None, None))?;
let first = registry.cancel(&id, Some(CancelReason::new("user")));
let second = registry.cancel(&id, Some(CancelReason::new("timeout")));
assert_eq!(first, CancelOutcome::Cancelled);
assert_eq!(second, CancelOutcome::Ignored);
assert_eq!(
registry
.get(&id)
.and_then(|entry| entry.cancellation().reason()),
Some(&AbortReason::Cancelled(Some(CancelReason::new("user"))))
);
Ok(())
}
#[test]
fn a_zero_budget_expires_immediately_and_an_absent_budget_never_expires() -> Result<(), Failure>
{
let mut registry = RequestRegistry::new();
let bounded = RequestId::number(1);
let unbounded = RequestId::number(2);
registry.register(
bounded.clone(),
entry(None, Some(Duration::from_millis(100))),
)?;
registry.register(unbounded.clone(), entry(None, None))?;
assert!(
registry.expire(at(99)).is_empty(),
"expired a millisecond early"
);
assert_eq!(registry.expire(at(100)), vec![bounded]);
assert_eq!(registry.len(), 1);
assert!(registry.expire(at(1_000_000)).is_empty());
assert!(registry.get(&unbounded).is_some());
Ok(())
}
#[test]
fn an_expired_request_is_aborted_so_its_handler_wakes() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let id = RequestId::number(1);
registry.register(id.clone(), entry(None, Some(Duration::from_secs(1))))?;
let token = handle(®istry, &id)?;
registry.expire(at(1_001));
assert!(token.is_cancelled());
assert_eq!(token.reason(), Some(&AbortReason::Expired));
Ok(())
}
#[test]
fn progress_needs_a_token_and_moves_forward_only() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
let without = RequestId::number(1);
let with = RequestId::number(2);
registry.register(without.clone(), entry(None, None))?;
registry.register(with.clone(), entry(Some(ProgressToken::text("job")), None))?;
let advanced = registry
.get_mut(&without)
.is_some_and(|entry| entry.advance_progress(1.0, None));
assert!(!advanced, "a request with no progress token never emits");
let entry = registry
.get_mut(&with)
.ok_or(Failure::Missing("the request carrying a progress token"))?;
assert!(entry.advance_progress(1.0, Some(String::from("reading"))));
assert!(!entry.advance_progress(1.0, None));
assert!(entry.advance_progress(2.0, None));
assert_eq!(entry.furthest_progress(), Some(2.0));
assert_eq!(entry.progress_token(), Some(&ProgressToken::text("job")));
Ok(())
}
#[test]
fn draining_aborts_everything_it_forgets() -> Result<(), Failure> {
let mut registry = RequestRegistry::new();
for n in 0..3 {
registry.register(RequestId::number(n), entry(None, None))?;
}
let tokens: Vec<_> = registry
.ids()
.map(|id| registry.get(id).map(InFlight::cancellation_handle))
.collect();
let drained = registry.drain();
assert_eq!(drained.len(), 3);
assert!(registry.is_empty());
assert_eq!(
tokens.iter().filter(|token| token.is_none()).count(),
0,
"every registered id had an entry before the drain"
);
for token in tokens.into_iter().flatten() {
assert_eq!(token.reason(), Some(&AbortReason::SessionClosed));
}
Ok(())
}
#[test]
fn an_error_message_redacts_the_id() {
let secret = RequestId::text("super-secret-credential");
let message = DuplicateRequest { id: secret.clone() }.to_string();
assert!(!message.contains("super-secret-credential"), "{message}");
assert!(message.contains(&secret.redacted()), "{message}");
let unknown = UnknownRequest { id: secret }.to_string();
assert!(!unknown.contains("super-secret-credential"), "{unknown}");
}
#[test]
fn both_origins_share_one_table() -> Result<(), Failure> {
assert_ne!(RequestOrigin::Inbound, RequestOrigin::Outbound);
let mut registry = RequestRegistry::new();
registry.register(
RequestId::number(1),
InFlight::new(
RequestOrigin::Outbound,
"sampling/createMessage",
None,
None,
at(0),
),
)?;
assert_eq!(
registry.get(&RequestId::number(1)).map(InFlight::origin),
Some(RequestOrigin::Outbound)
);
Ok(())
}
}