Skip to main content

mcp_usage_tower/
deferred.rs

1//! Terminal accounting that could not finish without awaiting.
2//!
3//! Accounting runs when the response body is released, and a body is released
4//! from `Drop`, where nothing may await. That is fine for synchronous recorders
5//! and for the in-process task store, whose futures never yield. A durable store
6//! backed by Redis or `PostgreSQL` does real I/O, so its first poll pends, and
7//! the work has nowhere to run.
8//!
9//! Dropping it there is not an option. The affected paths are the ones that
10//! carry durable-task attribution: the `insert` that records which call
11//! commissioned a task, and the `claim` that prices its completion. Losing them
12//! loses the charge for every durable task, in exactly the horizontally scaled
13//! deployment those stores exist for.
14//!
15//! So the unfinished future is parked here instead, and driven later from a
16//! context that can await. Two things drive it, deliberately: every subsequent
17//! authenticated request drains a bounded number, so an application that does
18//! nothing still converges, and [`DeferredCompletions::drain`] empties the queue
19//! on demand for timeliness and for shutdown.
20//!
21//! Parking never allocates a runtime and never spawns, which is what keeps this
22//! crate runtime agnostic.
23
24use std::collections::VecDeque;
25use std::future::Future;
26use std::pin::Pin;
27use std::sync::Mutex;
28use std::sync::atomic::{AtomicU64, Ordering};
29
30/// A parked terminal-accounting future.
31type Parked = Pin<Box<dyn Future<Output = ()> + Send>>;
32
33/// Queue of terminal accounting waiting for somewhere to await.
34///
35/// Obtain one from [`EdgeConfig::deferred`](crate::EdgeConfig::deferred) before
36/// handing the configuration to the layer, the same way edge metrics are
37/// obtained.
38pub struct DeferredCompletions {
39    queue: Mutex<VecDeque<Parked>>,
40    capacity: usize,
41    dropped: AtomicU64,
42}
43
44impl std::fmt::Debug for DeferredCompletions {
45    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        formatter
47            .debug_struct("DeferredCompletions")
48            .field("pending", &self.len())
49            .field("capacity", &self.capacity)
50            .field("dropped", &self.dropped.load(Ordering::Relaxed))
51            .finish_non_exhaustive()
52    }
53}
54
55impl DeferredCompletions {
56    pub(crate) fn new(capacity: usize) -> Self {
57        Self {
58            queue: Mutex::new(VecDeque::new()),
59            capacity,
60            dropped: AtomicU64::new(0),
61        }
62    }
63
64    /// Park an unfinished completion, reporting whether it was accepted.
65    ///
66    /// The queue is bounded, because an application that never drains must not
67    /// be able to grow it without limit. At capacity the *oldest* entry is
68    /// evicted: the newest accounting is the most likely to still be
69    /// claimable, and an unbounded backlog is worse than a bounded one.
70    pub(crate) fn park(&self, completion: Parked) -> bool {
71        let mut queue = self
72            .queue
73            .lock()
74            .unwrap_or_else(std::sync::PoisonError::into_inner);
75        if self.capacity == 0 {
76            self.record_drop();
77            return false;
78        }
79        let mut evicted = false;
80        while queue.len() >= self.capacity {
81            queue.pop_front();
82            evicted = true;
83        }
84        queue.push_back(completion);
85        if evicted {
86            self.record_drop();
87        }
88        !evicted
89    }
90
91    fn take_one(&self) -> Option<Parked> {
92        self.queue
93            .lock()
94            .unwrap_or_else(std::sync::PoisonError::into_inner)
95            .pop_front()
96    }
97
98    fn requeue_in_flight(&self, completion: Parked) {
99        let mut queue = self
100            .queue
101            .lock()
102            .unwrap_or_else(std::sync::PoisonError::into_inner);
103        if self.capacity == 0 {
104            self.record_drop();
105            return;
106        }
107        if queue.len() >= self.capacity {
108            queue.pop_back();
109            self.record_drop();
110        }
111        queue.push_front(completion);
112    }
113
114    fn record_drop(&self) {
115        let mut current = self.dropped.load(Ordering::Relaxed);
116        loop {
117            let next = current.saturating_add(1);
118            if next == current {
119                return;
120            }
121            match self.dropped.compare_exchange_weak(
122                current,
123                next,
124                Ordering::Relaxed,
125                Ordering::Relaxed,
126            ) {
127                Ok(_) => return,
128                Err(observed) => current = observed,
129            }
130        }
131    }
132
133    /// Run at most `limit` parked completions, returning how many ran.
134    ///
135    /// Entries are processed one at a time. If this drain is cancelled while a
136    /// completion is pending, a drop guard returns that exact future to the
137    /// front of the queue for a later drain.
138    pub async fn drain_some(&self, limit: usize) -> usize {
139        let mut completed = 0;
140        while completed < limit {
141            let Some(completion) = self.take_one() else {
142                break;
143            };
144            InFlight::new(self, completion).run().await;
145            completed += 1;
146        }
147        completed
148    }
149
150    /// Run every parked completion, returning how many ran.
151    ///
152    /// Call this from the same loop that flushes billing, and once more after
153    /// graceful shutdown, so durable-task accounting is not left behind by a
154    /// process that is about to exit.
155    pub async fn drain(&self) -> usize {
156        let mut total = 0;
157        loop {
158            let ran = self.drain_some(self.capacity.max(1)).await;
159            if ran == 0 {
160                return total;
161            }
162            total = total.saturating_add(ran);
163        }
164    }
165
166    /// Completions currently waiting to run.
167    #[must_use]
168    pub fn len(&self) -> usize {
169        self.queue
170            .lock()
171            .unwrap_or_else(std::sync::PoisonError::into_inner)
172            .len()
173    }
174
175    /// Whether nothing is waiting.
176    #[must_use]
177    pub fn is_empty(&self) -> bool {
178        self.len() == 0
179    }
180
181    /// Completions discarded because the queue was full.
182    ///
183    /// A nonzero value means usage was lost, and that draining is not keeping up
184    /// with the rate at which durable-task accounting is being parked.
185    #[must_use]
186    pub fn dropped(&self) -> u64 {
187        self.dropped.load(Ordering::Relaxed)
188    }
189}
190
191struct InFlight<'a> {
192    deferred: &'a DeferredCompletions,
193    completion: Option<Parked>,
194}
195
196impl<'a> InFlight<'a> {
197    fn new(deferred: &'a DeferredCompletions, completion: Parked) -> Self {
198        Self {
199            deferred,
200            completion: Some(completion),
201        }
202    }
203
204    async fn run(mut self) {
205        if let Some(completion) = self.completion.as_mut() {
206            completion.as_mut().await;
207        }
208        self.completion = None;
209    }
210}
211
212impl Drop for InFlight<'_> {
213    fn drop(&mut self) {
214        if let Some(completion) = self.completion.take() {
215            if std::thread::panicking() {
216                self.deferred.record_drop();
217            } else {
218                self.deferred.requeue_in_flight(completion);
219            }
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use std::sync::Arc;
228    use std::sync::atomic::AtomicUsize;
229
230    struct PendingUntilAllowed {
231        polls: Arc<AtomicUsize>,
232        allowed: Arc<std::sync::atomic::AtomicBool>,
233    }
234
235    impl Future for PendingUntilAllowed {
236        type Output = ();
237
238        fn poll(
239            self: Pin<&mut Self>,
240            _context: &mut std::task::Context<'_>,
241        ) -> std::task::Poll<()> {
242            self.polls.fetch_add(1, Ordering::Relaxed);
243            if self.allowed.load(Ordering::Relaxed) {
244                std::task::Poll::Ready(())
245            } else {
246                std::task::Poll::Pending
247            }
248        }
249    }
250
251    fn counting(counter: &Arc<AtomicUsize>) -> Parked {
252        let counter = Arc::clone(counter);
253        Box::pin(async move {
254            counter.fetch_add(1, Ordering::Relaxed);
255        })
256    }
257
258    #[tokio::test]
259    async fn parked_completions_run_when_drained() {
260        let ran = Arc::new(AtomicUsize::new(0));
261        let deferred = DeferredCompletions::new(8);
262        for _ in 0..3 {
263            assert!(deferred.park(counting(&ran)));
264        }
265        assert_eq!(deferred.len(), 3);
266
267        assert_eq!(deferred.drain().await, 3);
268        assert_eq!(ran.load(Ordering::Relaxed), 3);
269        assert!(deferred.is_empty());
270        assert_eq!(deferred.dropped(), 0);
271    }
272
273    #[tokio::test]
274    async fn draining_is_bounded_by_the_limit() {
275        let ran = Arc::new(AtomicUsize::new(0));
276        let deferred = DeferredCompletions::new(8);
277        for _ in 0..5 {
278            deferred.park(counting(&ran));
279        }
280
281        assert_eq!(deferred.drain_some(2).await, 2);
282        assert_eq!(ran.load(Ordering::Relaxed), 2);
283        assert_eq!(deferred.len(), 3, "the rest must stay queued");
284    }
285
286    #[tokio::test]
287    async fn a_full_queue_evicts_the_oldest_and_reports_the_loss() {
288        let ran = Arc::new(AtomicUsize::new(0));
289        let deferred = DeferredCompletions::new(2);
290        assert!(deferred.park(counting(&ran)));
291        assert!(deferred.park(counting(&ran)));
292        assert!(
293            !deferred.park(counting(&ran)),
294            "the third must report that something was discarded"
295        );
296
297        assert_eq!(deferred.len(), 2, "the queue must stay bounded");
298        assert_eq!(deferred.dropped(), 1);
299        assert_eq!(deferred.drain().await, 2);
300    }
301
302    #[tokio::test]
303    async fn a_zero_capacity_queue_accepts_nothing() {
304        let ran = Arc::new(AtomicUsize::new(0));
305        let deferred = DeferredCompletions::new(0);
306        assert!(!deferred.park(counting(&ran)));
307        assert!(deferred.is_empty());
308        assert_eq!(deferred.dropped(), 1);
309        assert_eq!(deferred.drain().await, 0);
310        assert_eq!(ran.load(Ordering::Relaxed), 0);
311    }
312
313    #[tokio::test]
314    async fn draining_an_empty_queue_is_a_no_op() {
315        let deferred = DeferredCompletions::new(4);
316        assert_eq!(deferred.drain().await, 0);
317        assert_eq!(deferred.drain_some(10).await, 0);
318    }
319
320    #[tokio::test]
321    async fn cancelling_a_drain_requeues_the_in_flight_completion() {
322        let deferred = Arc::new(DeferredCompletions::new(4));
323        let polls = Arc::new(AtomicUsize::new(0));
324        let allowed = Arc::new(std::sync::atomic::AtomicBool::new(false));
325        assert!(deferred.park(Box::pin(PendingUntilAllowed {
326            polls: Arc::clone(&polls),
327            allowed: Arc::clone(&allowed),
328        })));
329
330        let drain = tokio::spawn({
331            let deferred = Arc::clone(&deferred);
332            async move { deferred.drain_some(1).await }
333        });
334        while polls.load(Ordering::Relaxed) == 0 {
335            tokio::task::yield_now().await;
336        }
337        drain.abort();
338        assert!(drain.await.unwrap_err().is_cancelled());
339        assert_eq!(deferred.len(), 1);
340
341        allowed.store(true, Ordering::Relaxed);
342        assert_eq!(deferred.drain_some(1).await, 1);
343        assert!(deferred.is_empty());
344        assert_eq!(deferred.dropped(), 0);
345    }
346}