Skip to main content

rama_core/stream/
paced.rs

1use std::{
2    fmt,
3    future::Future,
4    pin::Pin,
5    task::{Context, Poll, ready},
6};
7
8use pin_project_lite::pin_project;
9use rama_utils::rate::{Acquire, Rate, RateLimiter, RefundWait};
10use tokio::time::{Sleep, sleep_until};
11
12use crate::bytes::{Bytes, BytesMut};
13use crate::futures::{Sink, Stream};
14
15pin_project! {
16    /// A [`Sink`] combinator that paces the items sent through it
17    /// against a token bucket: the go-to way to rate datagram flows.
18    ///
19    /// Wrap any framed sink — e.g. a `ConnectedUdpFramed`, a
20    /// `UdpFramed` or a unix datagram codec — and every item's cost
21    /// (its byte length, by default: see [`DatagramCost`]) is paid
22    /// from the budget:
23    ///
24    /// - [`Sink::start_send`] never blocks an item: its cost is
25    ///   recorded as *debt*;
26    /// - [`Sink::poll_ready`], [`Sink::poll_flush`] and [`Sink::poll_close`]
27    ///   repay outstanding debt (waiting on the bucket as needed).
28    ///
29    /// The long-run rate is exact, with at most one item of overshoot —
30    /// the right semantics for pacing, as a datagram is atomic.
31    /// Sending items larger than the burst capacity is fine: their debt
32    /// is repaid in burst-sized chunks.
33    ///
34    /// [`Stream`] is passed through, so a duplex frame transport stays
35    /// bridgeable (e.g. via [`StreamForwardService`]) after wrapping.
36    ///
37    /// To pace items-per-second rather than bytes-per-second, price
38    /// every item at one unit via [`PacedSink::with_cost_fn`].
39    ///
40    /// [`StreamForwardService`]: crate::stream::StreamForwardService
41    #[derive(Debug)]
42    pub struct PacedSink<S, C = ()> {
43        #[pin]
44        sink: S,
45        limiter: RateLimiter,
46        debt: u64,
47        sleep: Option<Pin<Box<Sleep>>>,
48        sleeping: bool,
49        refund_wait: Option<RefundWait>,
50        cost: C,
51    }
52}
53
54impl<S> PacedSink<S> {
55    /// Create a new [`PacedSink`] pacing at the given [`Rate`], with a
56    /// burst capacity of one period worth of units.
57    pub fn new(sink: S, rate: Rate) -> Self {
58        Self::with_limiter(sink, RateLimiter::from_rate(rate))
59    }
60
61    /// Create a new [`PacedSink`] pacing against a caller-provided
62    /// [`RateLimiter`]: clones of the handle share one aggregate
63    /// budget (e.g. an egress cap across many flows).
64    pub fn with_limiter(sink: S, limiter: RateLimiter) -> Self {
65        Self {
66            sink,
67            limiter,
68            debt: 0,
69            sleep: None,
70            sleeping: false,
71            refund_wait: None,
72            cost: (),
73        }
74    }
75
76    rama_utils::macros::generate_set_and_with! {
77        /// Override the burst capacity (default: one period worth of units).
78        ///
79        /// This rebuilds the sink's own [`RateLimiter`]: any previously
80        /// shared budget handle is disconnected.
81        pub fn burst(mut self, burst: u64) -> Self {
82            self.limiter = RateLimiter::new(self.limiter.rate(), burst);
83            self
84        }
85    }
86}
87
88impl<S, C> PacedSink<S, C> {
89    /// Price items with the given function instead of
90    /// their [`DatagramCost`].
91    ///
92    /// E.g. `|_| 1` paces items-per-second rather than bytes-per-second.
93    pub fn with_cost_fn<F>(self, cost_fn: F) -> PacedSink<S, CostFn<F>> {
94        PacedSink {
95            sink: self.sink,
96            limiter: self.limiter,
97            debt: self.debt,
98            sleep: self.sleep,
99            sleeping: self.sleeping,
100            refund_wait: self.refund_wait,
101            cost: CostFn(cost_fn),
102        }
103    }
104
105    /// The [`RateLimiter`] enforcing this sink's budget
106    /// (clone it to share the budget elsewhere).
107    #[must_use]
108    pub fn limiter(&self) -> &RateLimiter {
109        &self.limiter
110    }
111
112    /// Consume this combinator, returning the underlying sink.
113    pub fn into_inner(self) -> S {
114        self.sink
115    }
116
117    /// Get a reference to the underlying sink.
118    pub fn get_ref(&self) -> &S {
119        &self.sink
120    }
121}
122
123/// Prices the items sent through a [`PacedSink`].
124///
125/// The default coster `()` uses the item's own [`DatagramCost`];
126/// [`CostFn`] uses a closure instead.
127pub trait ItemCost<I> {
128    /// The cost of the given item, in rate units.
129    fn cost_of(&self, item: &I) -> u64;
130}
131
132impl<I: DatagramCost> ItemCost<I> for () {
133    fn cost_of(&self, item: &I) -> u64 {
134        item.cost()
135    }
136}
137
138/// An [`ItemCost`] pricing items with a closure,
139/// see [`PacedSink::with_cost_fn`].
140pub struct CostFn<F>(F);
141
142impl<F> fmt::Debug for CostFn<F> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.debug_struct("CostFn").finish()
145    }
146}
147
148impl<I, F: Fn(&I) -> u64> ItemCost<I> for CostFn<F> {
149    fn cost_of(&self, item: &I) -> u64 {
150        (self.0)(item)
151    }
152}
153
154/// The intrinsic cost of a datagram-ish item: its byte length.
155///
156/// This is what a [`PacedSink`] charges by default. The tuple impl
157/// covers address-carrying sinks such as `UdpFramed`
158/// (`(item, SocketAddr)`).
159pub trait DatagramCost {
160    /// The cost of this item, in rate units.
161    fn cost(&self) -> u64;
162}
163
164impl DatagramCost for Bytes {
165    fn cost(&self) -> u64 {
166        self.len() as u64
167    }
168}
169
170impl DatagramCost for BytesMut {
171    fn cost(&self) -> u64 {
172        self.len() as u64
173    }
174}
175
176impl DatagramCost for Vec<u8> {
177    fn cost(&self) -> u64 {
178        self.len() as u64
179    }
180}
181
182impl DatagramCost for Box<[u8]> {
183    fn cost(&self) -> u64 {
184        self.len() as u64
185    }
186}
187
188impl DatagramCost for &[u8] {
189    fn cost(&self) -> u64 {
190        self.len() as u64
191    }
192}
193
194impl DatagramCost for String {
195    fn cost(&self) -> u64 {
196        self.len() as u64
197    }
198}
199
200impl DatagramCost for &str {
201    fn cost(&self) -> u64 {
202        self.len() as u64
203    }
204}
205
206impl<T: DatagramCost, A> DatagramCost for (T, A) {
207    fn cost(&self) -> u64 {
208        self.0.cost()
209    }
210}
211
212fn poll_debt(
213    limiter: &RateLimiter,
214    debt: &mut u64,
215    sleep: &mut Option<Pin<Box<Sleep>>>,
216    sleeping: &mut bool,
217    refund_wait: &mut Option<RefundWait>,
218    cx: &mut Context<'_>,
219) -> Poll<()> {
220    while *debt > 0 {
221        if refund_wait
222            .as_mut()
223            .is_some_and(|wait| Pin::new(wait).poll(cx).is_ready())
224        {
225            *refund_wait = None;
226            *sleeping = false;
227            continue;
228        }
229        let want = (*debt).min(limiter.burst());
230        let mut acquire = limiter.try_acquire(want);
231        if matches!(acquire, Acquire::RetryAt(_)) && refund_wait.is_none() {
232            *refund_wait = Some(limiter.notified_on_refund());
233            if refund_wait
234                .as_mut()
235                .is_some_and(|wait| Pin::new(wait).poll(cx).is_ready())
236            {
237                *refund_wait = None;
238                *sleeping = false;
239                continue;
240            }
241            acquire = limiter.try_acquire(want);
242        }
243        match acquire {
244            Acquire::Granted => {
245                *refund_wait = None;
246                *sleeping = false;
247                *debt -= want;
248            }
249            Acquire::RetryAt(at) => {
250                let deadline = limiter.deadline(at);
251                let sleep = sleep.get_or_insert_with(|| Box::pin(sleep_until(deadline)));
252                if !*sleeping {
253                    sleep.as_mut().reset(deadline);
254                    *sleeping = true;
255                }
256                ready!(sleep.as_mut().poll(cx));
257                *sleeping = false;
258            }
259            Acquire::Never => {
260                debug_assert!(false, "burst-clamped repayment reported Acquire::Never");
261                *debt = 0;
262            }
263        }
264    }
265    Poll::Ready(())
266}
267
268impl<S, I, C> Sink<I> for PacedSink<S, C>
269where
270    S: Sink<I>,
271    C: ItemCost<I>,
272{
273    type Error = S::Error;
274
275    fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
276        let this = self.project();
277        ready!(poll_debt(
278            this.limiter,
279            this.debt,
280            this.sleep,
281            this.sleeping,
282            this.refund_wait,
283            cx,
284        ));
285        this.sink.poll_ready(cx)
286    }
287
288    fn start_send(self: Pin<&mut Self>, item: I) -> Result<(), Self::Error> {
289        let this = self.project();
290        // charge only once the item is actually accepted, so a failed
291        // send does not leave phantom debt that over-throttles the next item
292        let cost = this.cost.cost_of(&item);
293        this.sink.start_send(item)?;
294        *this.debt = this.debt.saturating_add(cost);
295        Ok(())
296    }
297
298    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
299        let this = self.project();
300        ready!(poll_debt(
301            this.limiter,
302            this.debt,
303            this.sleep,
304            this.sleeping,
305            this.refund_wait,
306            cx,
307        ));
308        this.sink.poll_flush(cx)
309    }
310
311    fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
312        let this = self.project();
313        ready!(poll_debt(
314            this.limiter,
315            this.debt,
316            this.sleep,
317            this.sleeping,
318            this.refund_wait,
319            cx,
320        ));
321        this.sink.poll_close(cx)
322    }
323}
324
325impl<S, C> Stream for PacedSink<S, C>
326where
327    S: Stream,
328{
329    type Item = S::Item;
330
331    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
332        self.project().sink.poll_next(cx)
333    }
334
335    #[inline(always)]
336    fn size_hint(&self) -> (usize, Option<usize>) {
337        self.sink.size_hint()
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::futures::SinkExt;
345    use std::convert::Infallible;
346    use std::time::Duration;
347    use tokio::time::Instant;
348
349    #[derive(Debug, Default)]
350    struct VecSink {
351        items: Vec<Bytes>,
352    }
353
354    impl Sink<Bytes> for VecSink {
355        type Error = Infallible;
356
357        fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
358            Poll::Ready(Ok(()))
359        }
360
361        fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
362            self.get_mut().items.push(item);
363            Ok(())
364        }
365
366        fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
367            Poll::Ready(Ok(()))
368        }
369
370        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
371            Poll::Ready(Ok(()))
372        }
373    }
374
375    #[derive(Debug, Default)]
376    struct PendingSink {
377        ready_polls: usize,
378        close_polls: usize,
379    }
380
381    impl Sink<Bytes> for PendingSink {
382        type Error = Infallible;
383
384        fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
385            self.get_mut().ready_polls += 1;
386            Poll::Pending
387        }
388
389        fn start_send(self: Pin<&mut Self>, _: Bytes) -> Result<(), Self::Error> {
390            Ok(())
391        }
392
393        fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
394            Poll::Pending
395        }
396
397        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
398            self.get_mut().close_polls += 1;
399            Poll::Pending
400        }
401    }
402
403    #[test]
404    fn costers_report_exact_costs() {
405        let cost = CostFn(|value: &usize| (*value as u64) + 2);
406        assert_eq!(format!("{cost:?}"), "CostFn");
407        assert_eq!(cost.cost_of(&5), 7);
408
409        let bytes_mut = BytesMut::from(&b"abc"[..]);
410        let vec = b"abc".to_vec();
411        let boxed = Vec::from(&b"abc"[..]).into_boxed_slice();
412        let slice: &[u8] = b"abc";
413        let string = String::from("abc");
414        let str_slice: &str = "abc";
415
416        assert_eq!(bytes_mut.cost(), 3);
417        assert_eq!(vec.cost(), 3);
418        assert_eq!(boxed.cost(), 3);
419        assert_eq!(slice.cost(), 3);
420        assert_eq!(string.cost(), 3);
421        assert_eq!(str_slice.cost(), 3);
422        assert_eq!((str_slice, "address").cost(), 3);
423    }
424
425    #[test]
426    fn sink_readiness_and_close_are_delegated() {
427        let mut sink = Box::pin(PacedSink::new(PendingSink::default(), Rate::per_sec(1)));
428        let mut cx = Context::from_waker(std::task::Waker::noop());
429
430        assert!(
431            <PacedSink<PendingSink> as Sink<Bytes>>::poll_ready(sink.as_mut(), &mut cx)
432                .is_pending()
433        );
434        assert_eq!(sink.as_ref().get_ref().get_ref().ready_polls, 1);
435
436        assert!(
437            <PacedSink<PendingSink> as Sink<Bytes>>::poll_close(sink.as_mut(), &mut cx)
438                .is_pending()
439        );
440        assert_eq!(sink.as_ref().get_ref().get_ref().close_polls, 1);
441    }
442
443    #[test]
444    fn stream_size_hint_is_delegated() {
445        let paced = PacedSink::new(crate::futures::stream::iter([1u8, 2, 3]), Rate::per_sec(1));
446        assert_eq!(Stream::size_hint(&paced), (3, Some(3)));
447    }
448
449    #[tokio::test(start_paused = true)]
450    async fn paces_by_byte_cost() {
451        let mut sink = PacedSink::new(VecSink::default(), Rate::per_sec(1_000));
452        let item = || Bytes::from_static(&[0u8; 500]);
453
454        let start = Instant::now();
455        // burst 1000: two items pass immediately ...
456        sink.send(item()).await.unwrap();
457        sink.send(item()).await.unwrap();
458        assert_eq!(start.elapsed(), Duration::ZERO);
459
460        // ... then each further item waits for its own debt to be paid
461        sink.send(item()).await.unwrap();
462        assert_eq!(start.elapsed(), Duration::from_millis(500));
463
464        sink.send(item()).await.unwrap();
465        assert_eq!(start.elapsed(), Duration::from_millis(1_000));
466
467        sink.send(item()).await.unwrap();
468        assert_eq!(start.elapsed(), Duration::from_millis(1_500));
469
470        assert_eq!(sink.get_ref().items.len(), 5);
471    }
472
473    #[tokio::test(start_paused = true)]
474    async fn paces_items_with_cost_fn() {
475        // 2 datagrams per second, independent of their size
476        let mut sink =
477            PacedSink::new(VecSink::default(), Rate::per_sec(2)).with_cost_fn(|_: &Bytes| 1);
478
479        let start = Instant::now();
480        for _ in 0..3 {
481            sink.send(Bytes::from_static(b"whatever")).await.unwrap();
482        }
483        assert_eq!(start.elapsed(), Duration::from_millis(500));
484
485        sink.send(Bytes::from_static(b"...")).await.unwrap();
486        assert_eq!(start.elapsed(), Duration::from_millis(1_000));
487
488        sink.send(Bytes::from_static(b"...")).await.unwrap();
489        assert_eq!(start.elapsed(), Duration::from_millis(1_500));
490    }
491
492    #[tokio::test(start_paused = true)]
493    async fn oversized_items_repay_in_chunks() {
494        let mut sink = PacedSink::new(VecSink::default(), Rate::per_sec(100)).with_burst(100);
495
496        let start = Instant::now();
497        // 250 > burst: accepted right away, then its debt is repaid in chunks
498        sink.send(Bytes::from(vec![0u8; 250])).await.unwrap();
499        // 100 (burst) + 100 @+1s + 50 @+1.5s
500        assert_eq!(start.elapsed(), Duration::from_millis(1_500));
501
502        sink.send(Bytes::from_static(b"x")).await.unwrap();
503        assert_eq!(start.elapsed(), Duration::from_millis(1_510));
504    }
505
506    #[tokio::test(start_paused = true)]
507    async fn shared_limiter_is_aggregate() {
508        let limiter = RateLimiter::from_rate(Rate::per_sec(1_000));
509        let mut sink_a = PacedSink::with_limiter(VecSink::default(), limiter.clone());
510        let mut sink_b = PacedSink::with_limiter(VecSink::default(), limiter);
511
512        let start = Instant::now();
513        sink_a.send(Bytes::from(vec![0u8; 800])).await.unwrap();
514        sink_b.send(Bytes::from(vec![0u8; 800])).await.unwrap();
515        assert_eq!(start.elapsed(), Duration::from_millis(600));
516
517        sink_a.send(Bytes::from(vec![0u8; 100])).await.unwrap();
518        assert_eq!(start.elapsed(), Duration::from_millis(700));
519
520        sink_b.send(Bytes::from(vec![0u8; 100])).await.unwrap();
521        assert_eq!(start.elapsed(), Duration::from_millis(800));
522    }
523
524    #[tokio::test(start_paused = true)]
525    async fn a_grant_replaces_a_stale_deadline() {
526        let limiter = RateLimiter::new(Rate::per_sec(100), 100);
527        assert_eq!(limiter.try_acquire(100), Acquire::Granted);
528
529        let mut debt = 100;
530        let mut sleep = None;
531        let mut sleeping = false;
532        let mut refund_wait = None;
533        let mut cx = Context::from_waker(std::task::Waker::noop());
534        assert!(
535            poll_debt(
536                &limiter,
537                &mut debt,
538                &mut sleep,
539                &mut sleeping,
540                &mut refund_wait,
541                &mut cx,
542            )
543            .is_pending()
544        );
545
546        tokio::time::advance(Duration::from_millis(10)).await;
547        debt = 1;
548        assert!(
549            poll_debt(
550                &limiter,
551                &mut debt,
552                &mut sleep,
553                &mut sleeping,
554                &mut refund_wait,
555                &mut cx,
556            )
557            .is_ready()
558        );
559        assert!(!sleeping);
560
561        debt = 10;
562        let start = Instant::now();
563        std::future::poll_fn(|cx| {
564            poll_debt(
565                &limiter,
566                &mut debt,
567                &mut sleep,
568                &mut sleeping,
569                &mut refund_wait,
570                cx,
571            )
572        })
573        .await;
574        assert_eq!(start.elapsed(), Duration::from_millis(100));
575    }
576
577    #[tokio::test(start_paused = true)]
578    async fn a_shared_refund_wakes_debt_immediately() {
579        let limiter = RateLimiter::new(Rate::per_sec(100), 100);
580        assert_eq!(limiter.try_acquire(100), Acquire::Granted);
581
582        let waiter_limiter = limiter.clone();
583        let waiter = tokio::spawn(async move {
584            let mut debt = 100;
585            let mut sleep = None;
586            let mut sleeping = false;
587            let mut refund_wait = None;
588            std::future::poll_fn(|cx| {
589                poll_debt(
590                    &waiter_limiter,
591                    &mut debt,
592                    &mut sleep,
593                    &mut sleeping,
594                    &mut refund_wait,
595                    cx,
596                )
597            })
598            .await;
599        });
600        tokio::task::yield_now().await;
601        assert!(!waiter.is_finished());
602
603        let start = Instant::now();
604        limiter.refund(100);
605        tokio::task::yield_now().await;
606        assert!(waiter.is_finished());
607        waiter.await.unwrap();
608        assert_eq!(start.elapsed(), Duration::ZERO);
609    }
610
611    #[tokio::test(start_paused = true)]
612    async fn final_send_spends_shared_budget_before_flush_completes() {
613        let limiter = RateLimiter::new(Rate::per_sec(100), 100);
614        let mut sink = PacedSink::with_limiter(VecSink::default(), limiter.clone());
615
616        sink.send(Bytes::from(vec![0u8; 50])).await.unwrap();
617        drop(sink);
618
619        assert_eq!(limiter.try_acquire(50), Acquire::Granted);
620        assert!(matches!(limiter.try_acquire(1), Acquire::RetryAt(_)));
621    }
622
623    #[tokio::test(start_paused = true)]
624    async fn failed_start_send_charges_no_debt() {
625        // rejects its first item, accepts the rest
626        struct FlakySink {
627            reject_next: bool,
628            items: Vec<Bytes>,
629        }
630
631        impl Sink<Bytes> for FlakySink {
632            type Error = &'static str;
633
634            fn poll_ready(
635                self: Pin<&mut Self>,
636                _: &mut Context<'_>,
637            ) -> Poll<Result<(), Self::Error>> {
638                Poll::Ready(Ok(()))
639            }
640
641            fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
642                let this = self.get_mut();
643                if this.reject_next {
644                    this.reject_next = false;
645                    return Err("rejected");
646                }
647                this.items.push(item);
648                Ok(())
649            }
650
651            fn poll_flush(
652                self: Pin<&mut Self>,
653                _: &mut Context<'_>,
654            ) -> Poll<Result<(), Self::Error>> {
655                Poll::Ready(Ok(()))
656            }
657
658            fn poll_close(
659                self: Pin<&mut Self>,
660                _: &mut Context<'_>,
661            ) -> Poll<Result<(), Self::Error>> {
662                Poll::Ready(Ok(()))
663            }
664        }
665
666        let mut sink = PacedSink::new(
667            FlakySink {
668                reject_next: true,
669                items: Vec::new(),
670            },
671            Rate::per_sec(100),
672        )
673        .with_burst(100);
674
675        let start = Instant::now();
676        // a big item is rejected: its cost must not be charged as debt
677        let err = sink.send(Bytes::from(vec![0u8; 1_000])).await.unwrap_err();
678        assert_eq!(err, "rejected");
679
680        // a within-burst item now sends immediately; a phantom 1_000-unit
681        // debt from the reject would have forced a ~9s wait here.
682        sink.send(Bytes::from(vec![0u8; 50])).await.unwrap();
683        assert_eq!(start.elapsed(), Duration::ZERO);
684        assert_eq!(sink.get_ref().items.len(), 1);
685    }
686
687    #[tokio::test(start_paused = true)]
688    async fn stream_is_passed_through() {
689        use crate::futures::StreamExt;
690
691        let inner = crate::futures::stream::iter([1u8, 2, 3]);
692        // pin on the stack: a pure Stream wrapped in PacedSink
693        let paced = PacedSink::new(inner, Rate::per_sec(1));
694        let items: Vec<_> = paced.collect().await;
695        assert_eq!(items, [1, 2, 3]);
696    }
697}