Skip to main content

rustev_remote_http/
seam_queue.rs

1//! Bounded callback seams (spec 042, 3.12, I-3).
2//!
3//! Every remote context and sink client declares an in-flight limit and a
4//! queue capacity, each 1 to 65,536 with no default. A call that finds the
5//! queue full is never enqueued: it is refused at once and counted. A call
6//! that waits for an in-flight slot waits no longer than its own deadline.
7//! A response that arrives after its caller stopped waiting is discarded and
8//! counted as late. Memory is bounded by the two limits, because every
9//! admitted call holds a slot until its exchange ends.
10
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use tokio::sync::{OwnedSemaphorePermit, Semaphore};
15use tokio::time::Instant;
16
17/// The greatest in-flight limit or queue capacity (3.12.1).
18pub const MAX_SEAM_LIMIT: u32 = 65_536;
19
20/// The failure reason a full queue reports (3.12.1).
21pub const QUEUE_FULL: &str = "remote_seam_queue_full";
22
23/// A seam's counters, for metrics (3.12.4).
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct SeamCounters {
26    /// Calls admitted and not yet finished: in flight plus queued.
27    pub depth: u64,
28    /// Calls refused because the queue was full.
29    pub refused: u64,
30    /// Responses that arrived after their caller stopped waiting.
31    pub late_responses_discarded: u64,
32}
33
34#[derive(Default)]
35struct Counters {
36    refused: AtomicU64,
37    late: AtomicU64,
38}
39
40/// One seam's bounded queue. Clones share the queue.
41#[derive(Clone)]
42pub struct SeamQueue {
43    active: Arc<Semaphore>,
44    slots: Arc<Semaphore>,
45    total: usize,
46    counters: Arc<Counters>,
47}
48
49impl std::fmt::Debug for SeamQueue {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("SeamQueue")
52            .field("counters", &self.counters())
53            .finish()
54    }
55}
56
57/// The queue was full; the call was not enqueued.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct QueueFull;
60
61/// The call's deadline passed while it waited for an in-flight slot.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct QueueDeadline;
64
65/// An admitted call, queued until [`Slot::start`] returns.
66pub struct Slot {
67    slot: OwnedSemaphorePermit,
68    active: Arc<Semaphore>,
69}
70
71/// An admitted call holding an in-flight slot.
72pub struct Running {
73    _slot: OwnedSemaphorePermit,
74    _active: OwnedSemaphorePermit,
75}
76
77impl SeamQueue {
78    /// Both limits must lie in 1 to [`MAX_SEAM_LIMIT`].
79    pub fn new(in_flight: u32, capacity: u32) -> Result<Self, String> {
80        for (name, value) in [("in-flight limit", in_flight), ("queue capacity", capacity)] {
81            if !(1..=MAX_SEAM_LIMIT).contains(&value) {
82                return Err(format!("{name} must be 1 to {MAX_SEAM_LIMIT}"));
83            }
84        }
85        let total = (in_flight + capacity) as usize;
86        Ok(SeamQueue {
87            active: Arc::new(Semaphore::new(in_flight as usize)),
88            slots: Arc::new(Semaphore::new(total)),
89            total,
90            counters: Arc::default(),
91        })
92    }
93
94    /// Admit a call, or refuse it at once when the queue is full.
95    pub fn try_enter(&self) -> Result<Slot, QueueFull> {
96        match self.slots.clone().try_acquire_owned() {
97            Ok(slot) => Ok(Slot {
98                slot,
99                active: self.active.clone(),
100            }),
101            Err(_) => {
102                self.counters.refused.fetch_add(1, Ordering::Relaxed);
103                Err(QueueFull)
104            }
105        }
106    }
107
108    /// Count a response discarded because its caller stopped waiting.
109    pub fn count_late(&self) {
110        self.counters.late.fetch_add(1, Ordering::Relaxed);
111    }
112
113    pub fn counters(&self) -> SeamCounters {
114        SeamCounters {
115            depth: (self.total - self.slots.available_permits()) as u64,
116            refused: self.counters.refused.load(Ordering::Relaxed),
117            late_responses_discarded: self.counters.late.load(Ordering::Relaxed),
118        }
119    }
120}
121
122impl Slot {
123    /// Wait for an in-flight slot until `deadline`.
124    pub async fn start(self, deadline: Instant) -> Result<Running, QueueDeadline> {
125        match tokio::time::timeout_at(deadline, self.active.clone().acquire_owned()).await {
126            Ok(Ok(active)) => Ok(Running {
127                _slot: self.slot,
128                _active: active,
129            }),
130            _ => Err(QueueDeadline),
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::time::Duration;
139
140    #[test]
141    fn limits_are_declared_and_bounded() {
142        assert!(SeamQueue::new(0, 1).is_err());
143        assert!(SeamQueue::new(1, 0).is_err());
144        assert!(SeamQueue::new(MAX_SEAM_LIMIT + 1, 1).is_err());
145        assert!(SeamQueue::new(1, MAX_SEAM_LIMIT + 1).is_err());
146        assert!(SeamQueue::new(MAX_SEAM_LIMIT, MAX_SEAM_LIMIT).is_ok());
147    }
148
149    #[tokio::test(start_paused = true)]
150    async fn a_full_queue_refuses_and_a_queued_call_honours_its_deadline() {
151        let q = SeamQueue::new(1, 1).unwrap();
152        let running = q.try_enter().unwrap();
153        let running = running.start(Instant::now()).await.unwrap();
154        let queued = q.try_enter().unwrap();
155        assert_eq!(q.try_enter().err(), Some(QueueFull));
156        assert_eq!(
157            q.counters(),
158            SeamCounters {
159                depth: 2,
160                refused: 1,
161                late_responses_discarded: 0
162            }
163        );
164        let deadline = Instant::now() + Duration::from_millis(5);
165        assert_eq!(queued.start(deadline).await.err(), Some(QueueDeadline));
166        assert_eq!(q.counters().depth, 1);
167        drop(running);
168        assert_eq!(q.counters().depth, 0);
169        q.count_late();
170        assert_eq!(q.counters().late_responses_discarded, 1);
171    }
172}