rama_utils/rate/mod.rs
1//! Rate primitives: express "N units per period" and enforce it.
2//!
3//! The core of this module is sans-IO and `no_std`: [`TokenBucket`] never
4//! reads a clock, the caller provides time as monotonic nanoseconds. This
5//! keeps it trivially testable and embeddable in other sans-IO state
6//! machines (e.g. a send scheduler pacing datagrams).
7//!
8//! On top (behind the `std` feature) [`RateLimiter`] wraps a [`TokenBucket`]
9//! in a cheap-to-clone shared handle that can wait (pace) or reject
10//! (rate limit) using tokio timers.
11//!
12//! Units are whatever you want them to be: requests, connections,
13//! messages, bytes.
14
15mod bucket;
16#[doc(inline)]
17pub use bucket::TokenBucket;
18
19#[cfg(feature = "std")]
20mod limiter;
21#[cfg(feature = "std")]
22#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
23#[doc(inline)]
24pub use limiter::{RateLimiter, RefundWait};
25
26use core::time::Duration;
27
28/// A rate: `units` per `per` period.
29///
30/// Units are dimensionless: use it for requests, connections, messages
31/// or bytes alike. For byte rates [`Rate::per_sec`] *is* bytes/sec;
32/// compose with [`crate::octets`] helpers for larger units.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct Rate {
35 units: u64,
36 per: Duration,
37}
38
39impl Rate {
40 /// Create a new [`Rate`] of `units` per `per` period.
41 ///
42 /// # Panics
43 ///
44 /// Panics if `units` is zero, `per` is zero, or `per` exceeds
45 /// `u64::MAX` nanoseconds (~584 years). In const contexts this is
46 /// a compile-time error. Use [`Rate::try_new`] for fallible
47 /// construction from dynamic input.
48 ///
49 /// # Examples
50 ///
51 /// ```
52 /// use std::time::Duration;
53 /// use rama_utils::rate::Rate;
54 ///
55 /// const RATE: Rate = Rate::new(100, Duration::from_millis(250));
56 /// assert_eq!(RATE.units(), 100);
57 /// assert_eq!(RATE.per(), Duration::from_millis(250));
58 /// ```
59 #[must_use]
60 pub const fn new(units: u64, per: Duration) -> Self {
61 assert!(units != 0, "Rate: units must be non-zero");
62 assert!(
63 per.as_nanos() != 0 && per.as_nanos() <= u64::MAX as u128,
64 "Rate: per must be non-zero and at most u64::MAX nanos"
65 );
66 Self { units, per }
67 }
68
69 /// Create a new [`Rate`] of `units` per `per` period,
70 /// or `None` for the invalid inputs that make [`Rate::new`] panic.
71 #[must_use]
72 pub const fn try_new(units: u64, per: Duration) -> Option<Self> {
73 if units == 0 || per.as_nanos() == 0 || per.as_nanos() > u64::MAX as u128 {
74 return None;
75 }
76 Some(Self { units, per })
77 }
78
79 /// Create a new [`Rate`] of `units` per second.
80 ///
81 /// For byte rates this *is* bytes per second; combine with
82 /// [`crate::octets::kib_u64`], [`crate::octets::mib_u64`] and
83 /// [`crate::octets::gib_u64`] for KiB/MiB/GiB rates, and with
84 /// [`crate::octets::from_bits_u64`] for link rates expressed in bits.
85 ///
86 /// # Panics
87 ///
88 /// Panics if `units` is zero (compile-time error in const contexts).
89 ///
90 /// # Examples
91 ///
92 /// ```
93 /// use rama_utils::{octets, rate::Rate};
94 ///
95 /// // 100 requests per second
96 /// const REQ_RATE: Rate = Rate::per_sec(100);
97 /// # let _ = REQ_RATE;
98 /// // 10 MiB per second
99 /// const BYTE_RATE: Rate = Rate::per_sec(octets::mib_u64(10));
100 /// # let _ = BYTE_RATE;
101 /// // a 100 Mbit/s link rate
102 /// const LINK_RATE: Rate = Rate::per_sec(octets::from_bits_u64(100_000_000));
103 /// # let _ = LINK_RATE;
104 /// ```
105 #[must_use]
106 pub const fn per_sec(units: u64) -> Self {
107 Self::new(units, Duration::from_secs(1))
108 }
109
110 /// The number of units per [`Rate::per`] period.
111 #[must_use]
112 pub const fn units(&self) -> u64 {
113 self.units
114 }
115
116 /// The period over which [`Rate::units`] units are allowed.
117 #[must_use]
118 pub const fn per(&self) -> Duration {
119 self.per
120 }
121
122 pub(crate) const fn per_nanos(&self) -> u64 {
123 // validated by the constructors to fit
124 self.per.as_nanos() as u64
125 }
126}
127
128/// The result of [`TokenBucket::try_acquire`] (and
129/// [`RateLimiter::try_acquire`]).
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Acquire {
132 /// The requested units were spent; the caller may proceed.
133 Granted,
134 /// Not enough budget: the earliest monotonic instant (in the caller's
135 /// clock, nanoseconds) at which the requested units can be granted.
136 RetryAt(u64),
137 /// The request exceeds the burst capacity and can never be granted
138 /// as a single acquisition. Split it up (e.g. in burst-sized chunks,
139 /// as [`RateLimiter::acquire`] does) or reject it.
140 Never,
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn rate_valid() {
149 let rate = Rate::per_sec(10);
150 assert_eq!(rate.units(), 10);
151 assert_eq!(rate.per(), Duration::from_secs(1));
152 assert_eq!(rate.per_nanos(), 1_000_000_000);
153 }
154
155 #[test]
156 fn rate_try_new_invalid() {
157 assert!(Rate::try_new(0, Duration::from_secs(1)).is_none());
158 assert!(Rate::try_new(1, Duration::ZERO).is_none());
159 assert!(Rate::try_new(1, Duration::MAX).is_none());
160 assert!(Rate::try_new(1, Duration::from_nanos(1)).is_some());
161 }
162
163 #[test]
164 #[should_panic(expected = "non-zero")]
165 fn rate_new_zero_units_panics() {
166 assert_eq!(Rate::new(0, Duration::from_secs(1)).units(), 0);
167 }
168}