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