http_rate/quota.rs
1use core::{convert::TryInto, fmt, num::NonZeroU32, time::Duration};
2
3use crate::nanos::Nanos;
4
5/// A rate-limiting quota.
6///
7/// Quotas are expressed in a positive number of "cells" (the maximum number of positive decisions /
8/// allowed items until the rate limiter needs to replenish) and the amount of time for the rate
9/// limiter to replenish a single cell.
10///
11/// Neither the number of cells nor the replenishment unit of time may be zero.
12///
13/// # Burst sizes
14/// There are multiple ways of expressing the same quota: a quota given as `Quota::per_second(1)`
15/// allows, on average, the same number of cells through as a quota given as `Quota::per_minute(60)`.
16/// However, the quota of `Quota::per_minute(60)` has a burst size of 60 cells, meaning it is
17/// possible to accommodate 60 cells in one go, after which the equivalent of a minute of inactivity
18/// is required for the burst allowance to be fully restored.
19///
20/// Burst size gets really important when you construct a rate limiter that should allow multiple
21/// elements through at one time (using [`RateLimiter.check_n`](struct.RateLimiter.html#method.check_n)
22/// and its related functions): Only
23/// at most as many cells can be let through in one call as are given as the burst size.
24///
25/// In other words, the burst size is the maximum number of cells that the rate limiter will ever
26/// allow through without replenishing them.
27#[derive(Debug, PartialEq, Eq, Clone, Copy)]
28pub struct Quota {
29 pub(crate) max_burst: NonZeroU32,
30 pub(crate) replenish_1_per: Duration,
31}
32
33impl Quota {
34 /// Construct a quota for a number of cells per second. The given number of cells is also
35 /// assumed to be the maximum burst size.
36 ///
37 /// # Panics
38 /// - When max_burst is zero.
39 pub fn per_second<B>(max_burst: B) -> Self
40 where
41 B: TryInto<NonZeroU32>,
42 B::Error: fmt::Debug,
43 {
44 let max_burst = max_burst.try_into().unwrap();
45 let replenish_interval_ns = Duration::from_secs(1).as_nanos() / (max_burst.get() as u128);
46 Self::new(max_burst, replenish_interval_ns)
47 }
48
49 /// Construct a quota for a number of cells per 60-second period. The given number of cells is
50 /// also assumed to be the maximum burst size.
51 ///
52 /// # Panics
53 /// - When max_burst is zero.
54 pub fn per_minute<B>(max_burst: B) -> Self
55 where
56 B: TryInto<NonZeroU32>,
57 B::Error: fmt::Debug,
58 {
59 let max_burst = max_burst.try_into().unwrap();
60 let replenish_interval_ns = Duration::from_secs(60).as_nanos() / (max_burst.get() as u128);
61 Self::new(max_burst, replenish_interval_ns)
62 }
63
64 /// Construct a quota for a number of cells per 60-minute (3600-second) period. The given number
65 /// of cells is also assumed to be the maximum burst size.
66 ///
67 /// # Panics
68 /// - When max_burst is zero.
69 pub fn per_hour<B>(max_burst: B) -> Self
70 where
71 B: TryInto<NonZeroU32>,
72 B::Error: fmt::Debug,
73 {
74 let max_burst = max_burst.try_into().unwrap();
75 let replenish_interval_ns = Duration::from_secs(60 * 60).as_nanos() / (max_burst.get() as u128);
76 Self::new(max_burst, replenish_interval_ns)
77 }
78
79 /// Construct a quota that replenishes one cell in a given
80 /// interval.
81 ///
82 /// This constructor is meant to replace [`::new`](#method.new),
83 /// in cases where a longer refresh period than 1 cell/hour is
84 /// necessary.
85 ///
86 /// If the time interval is zero, returns `None`.
87 pub fn with_period(replenish_1_per: Duration) -> Option<Quota> {
88 if replenish_1_per.as_nanos() == 0 {
89 None
90 } else {
91 Some(Quota {
92 max_burst: NonZeroU32::new(1).unwrap(),
93 replenish_1_per,
94 })
95 }
96 }
97
98 /// Adjusts the maximum burst size for a quota to construct a rate limiter with a capacity
99 /// for at most the given number of cells.
100 ///
101 /// # Panics
102 /// - When max_burst is zero.
103 pub fn allow_burst<B>(mut self, max_burst: B) -> Self
104 where
105 B: TryInto<NonZeroU32>,
106 B::Error: fmt::Debug,
107 {
108 self.max_burst = max_burst.try_into().unwrap();
109 self
110 }
111}
112
113impl Quota {
114 fn new(max_burst: NonZeroU32, dur_ns: u128) -> Self {
115 Self {
116 max_burst,
117 replenish_1_per: Duration::from_nanos(dur_ns as u64),
118 }
119 }
120
121 // The maximum number of cells that can be allowed in one burst.
122 pub(crate) const fn burst_size(&self) -> NonZeroU32 {
123 self.max_burst
124 }
125
126 #[cfg(test)]
127 // The time it takes for a rate limiter with an exhausted burst budget to replenish
128 // a single element.
129 const fn replenish_interval(&self) -> Duration {
130 self.replenish_1_per
131 }
132
133 // The time it takes to replenish the entire maximum burst size.
134 // const fn burst_size_replenished_in(&self) -> Duration {
135 // let fill_in_ns = self.replenish_1_per.as_nanos() * self.max_burst.get() as u128;
136 // Duration::from_nanos(fill_in_ns as u64)
137 // }
138}
139
140impl Quota {
141 // A way to reconstruct a Quota from an in-use Gcra.
142 pub(crate) fn from_gcra_parameters(t: Nanos, tau: Nanos) -> Quota {
143 let max_burst = NonZeroU32::new((tau.as_u64() / t.as_u64()) as u32).unwrap();
144 let replenish_1_per = t.into();
145 Quota {
146 max_burst,
147 replenish_1_per,
148 }
149 }
150}
151
152#[cfg(test)]
153mod test {
154 use super::*;
155
156 #[test]
157 fn time_multiples() {
158 let hourly = Quota::per_hour(1);
159 let minutely = Quota::per_minute(1);
160 let secondly = Quota::per_second(1);
161
162 assert_eq!(hourly.replenish_interval() / 60, minutely.replenish_interval());
163 assert_eq!(minutely.replenish_interval() / 60, secondly.replenish_interval());
164 }
165
166 #[test]
167 fn period_error_cases() {
168 assert!(Quota::with_period(Duration::from_secs(0)).is_none());
169 }
170}