governor/quota.rs
1use core::num::NonZeroU32;
2use core::time::Duration;
3use nonzero_ext::nonzero;
4
5use crate::nanos::Nanos;
6
7/// A rate-limiting quota.
8///
9/// Quotas are expressed in a positive number of "cells" (the maximum number of positive decisions /
10/// allowed items until the rate limiter needs to replenish) and the amount of time for the rate
11/// limiter to replenish a single cell.
12///
13/// Neither the number of cells nor the replenishment unit of time may be zero.
14///
15/// # Burst sizes
16/// There are multiple ways of expressing the same quota: a quota given as `Quota::per_second(1)`
17/// allows, on average, the same number of cells through as a quota given as `Quota::per_minute(60)`.
18/// However, the quota of `Quota::per_minute(60)` has a burst size of 60 cells, meaning it is
19/// possible to accomodate 60 cells in one go, after which the equivalent of a minute of inactivity
20/// is required for the burst allowance to be fully restored.
21///
22/// Burst size gets really important when you construct a rate limiter that should allow multiple
23/// elements through at one time (using [`RateLimiter.check_n`](struct.RateLimiter.html#method.check_n)
24/// and its related functions): Only
25/// at most as many cells can be let through in one call as are given as the burst size.
26///
27/// In other words, the burst size is the maximum number of cells that the rate limiter will ever
28/// allow through without replenishing them.
29///
30/// # Examples
31///
32/// Construct a quota that allows 50 cells per second (replenishing at a rate of one cell
33/// per 20 milliseconds), with a burst size of 50 cells, allowing a full rate limiter to allow 50
34/// cells through at a time:
35/// ```rust
36/// # use governor::Quota;
37/// # use nonzero_ext::nonzero;
38/// # use std::time::Duration;
39/// let q = Quota::per_second(nonzero!(50u32));
40/// assert_eq!(q, Quota::per_second(nonzero!(50u32)).allow_burst(nonzero!(50u32)));
41/// assert_eq!(q.replenish_interval(), Duration::from_millis(20));
42/// assert_eq!(q.burst_size().get(), 50);
43/// // The Quota::new constructor is deprecated, but this constructs the equivalent quota:
44/// #[allow(deprecated)]
45/// assert_eq!(q, Quota::new(nonzero!(50u32), Duration::from_secs(1)).unwrap());
46/// ```
47///
48/// Construct a quota that allows 2 cells per hour through (replenishing at a rate of one cell
49/// per 30min), but allows bursting up to 90 cells at once:
50/// ```rust
51/// # use governor::Quota;
52/// # use nonzero_ext::nonzero;
53/// # use std::time::Duration;
54/// let q = Quota::per_hour(nonzero!(2u32)).allow_burst(nonzero!(90u32));
55/// assert_eq!(q.replenish_interval(), Duration::from_secs(30 * 60));
56/// assert_eq!(q.burst_size().get(), 90);
57/// // The entire maximum burst size will be restored if no cells are let through for 45 hours:
58/// assert_eq!(q.burst_size_replenished_in(), Duration::from_secs(60 * 60 * (90 / 2)));
59/// ```
60#[derive(Debug, PartialEq, Eq, Clone, Copy)]
61pub struct Quota {
62 pub(crate) max_burst: NonZeroU32,
63 pub(crate) replenish_1_per: Duration,
64}
65
66/// Constructors for Quotas
67impl Quota {
68 /// Construct a quota for a number of cells per second. The given number of cells is also
69 /// assumed to be the maximum burst size.
70 pub const fn per_second(max_burst: NonZeroU32) -> Quota {
71 let replenish_interval_ns = Duration::from_secs(1).as_nanos() / (max_burst.get() as u128);
72 Quota {
73 max_burst,
74 replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
75 }
76 }
77
78 /// Construct a quota for a number of cells per 60-second period. The given number of cells is
79 /// also assumed to be the maximum burst size.
80 pub const fn per_minute(max_burst: NonZeroU32) -> Quota {
81 let replenish_interval_ns = Duration::from_secs(60).as_nanos() / (max_burst.get() as u128);
82 Quota {
83 max_burst,
84 replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
85 }
86 }
87
88 /// Construct a quota for a number of cells per 60-minute (3600-second) period. The given number
89 /// of cells is also assumed to be the maximum burst size.
90 pub const fn per_hour(max_burst: NonZeroU32) -> Quota {
91 let replenish_interval_ns =
92 Duration::from_secs(60 * 60).as_nanos() / (max_burst.get() as u128);
93 Quota {
94 max_burst,
95 replenish_1_per: Duration::from_nanos(replenish_interval_ns as u64),
96 }
97 }
98
99 /// Construct a quota that replenishes one cell in a given
100 /// interval.
101 ///
102 /// This constructor is meant to replace [`::new`](#method.new),
103 /// in cases where a longer refresh period than 1 cell/hour is
104 /// necessary.
105 ///
106 /// If the time interval is zero, returns `None`.
107 ///
108 /// # Example
109 /// ```rust
110 /// # use nonzero_ext::nonzero;
111 /// # use governor::Quota;
112 /// # use std::time::Duration;
113 /// // Replenish one cell per day, with a burst capacity of 10 cells:
114 /// let _quota = Quota::with_period(Duration::from_secs(60 * 60 * 24))
115 /// .unwrap()
116 /// .allow_burst(nonzero!(10u32));
117 /// ```
118 pub fn with_period(replenish_1_per: Duration) -> Option<Quota> {
119 if replenish_1_per.as_nanos() == 0 {
120 None
121 } else {
122 Some(Quota {
123 max_burst: nonzero!(1u32),
124 replenish_1_per,
125 })
126 }
127 }
128
129 /// Adjusts the maximum burst size for a quota to construct a rate limiter with a capacity
130 /// for at most the given number of cells.
131 pub const fn allow_burst(self, max_burst: NonZeroU32) -> Quota {
132 Quota { max_burst, ..self }
133 }
134
135 /// Construct a quota for a given burst size, replenishing the entire burst size in that
136 /// given unit of time.
137 ///
138 /// Returns `None` if the duration is zero.
139 ///
140 /// This constructor allows greater control over the resulting
141 /// quota, but doesn't make as much intuitive sense as other
142 /// methods of constructing the same quotas. Unless your quotas
143 /// are given as "max burst size, and time it takes to replenish
144 /// that burst size", you are better served by the
145 /// [`Quota::per_second`](#method.per_second) (and similar)
146 /// constructors with the [`allow_burst`](#method.allow_burst)
147 /// modifier.
148 #[deprecated(
149 since = "0.2.0",
150 note = "This constructor is often confusing and non-intuitive. \
151 Use the `per_(interval)` / `with_period` and `max_burst` constructors instead."
152 )]
153 pub fn new(max_burst: NonZeroU32, replenish_all_per: Duration) -> Option<Quota> {
154 if replenish_all_per.as_nanos() == 0 {
155 None
156 } else {
157 Some(Quota {
158 max_burst,
159 replenish_1_per: replenish_all_per / max_burst.get(),
160 })
161 }
162 }
163}
164
165/// Retrieving information about a quota
166impl Quota {
167 /// The time it takes for a rate limiter with an exhausted burst budget to replenish
168 /// a single element.
169 pub const fn replenish_interval(&self) -> Duration {
170 self.replenish_1_per
171 }
172
173 /// The maximum number of cells that can be allowed in one burst.
174 pub const fn burst_size(&self) -> NonZeroU32 {
175 self.max_burst
176 }
177
178 /// The time it takes to replenish the entire maximum burst size.
179 pub const fn burst_size_replenished_in(&self) -> Duration {
180 let fill_in_ns = self.replenish_1_per.as_nanos() * self.max_burst.get() as u128;
181 Duration::from_nanos(fill_in_ns as u64)
182 }
183}
184
185impl Quota {
186 /// A way to reconstruct a Quota from an in-use Gcra.
187 ///
188 /// This is useful mainly for [`crate::middleware::RateLimitingMiddleware`]
189 /// where custom code may want to construct information based on
190 /// the amount of burst balance remaining.
191 pub(crate) fn from_gcra_parameters(t: Nanos, tau: Nanos) -> Quota {
192 // Safety assurance: by construction, the computed value is bounded by
193 // one at the lower.
194 //
195 // The casts may look a little sketch, but they're constructed from
196 // parameters that came from the crate exactly like that.
197 let max_burst =
198 unsafe { NonZeroU32::new_unchecked(1 + (tau.as_u64() / t.as_u64()) as u32) };
199 let replenish_1_per = t.into();
200 Quota {
201 max_burst,
202 replenish_1_per,
203 }
204 }
205}
206
207#[cfg(test)]
208mod test {
209 use super::*;
210 use nonzero_ext::nonzero;
211
212 #[test]
213 fn time_multiples() {
214 let hourly = Quota::per_hour(nonzero!(1u32));
215 let minutely = Quota::per_minute(nonzero!(1u32));
216 let secondly = Quota::per_second(nonzero!(1u32));
217
218 assert_eq!(
219 hourly.replenish_interval() / 60,
220 minutely.replenish_interval()
221 );
222 assert_eq!(
223 minutely.replenish_interval() / 60,
224 secondly.replenish_interval()
225 );
226 }
227
228 #[test]
229 fn period_error_cases() {
230 assert!(Quota::with_period(Duration::from_secs(0)).is_none());
231
232 #[allow(deprecated)]
233 {
234 assert!(Quota::new(nonzero!(1u32), Duration::from_secs(0)).is_none());
235 }
236 }
237}