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}