rl_core/tracker.rs
1use crate::Duration as _;
2
3/// State for a single rate limit.
4///
5/// This tracks the state of a single rate limit. For example the rate limit for a particular IP.
6///
7/// This object is intentionally small and cheap so that it can be created for fine-grained tracking.
8#[cfg_attr(test, derive(PartialEq))]
9#[derive(Clone,Debug)]
10pub enum Tracker<T: crate::Time = std::time::SystemTime> {
11 #[doc(hidden)]
12 Filling{
13 /// The time when the tracker was last empty. It has been filling since then.
14 empty_at: T,
15 },
16 #[doc(hidden)]
17 Full{
18 /// Extra credits over the regular burst capacity.
19 overfull: u32,
20 },
21 #[doc(hidden)]
22 Unlimited,
23}
24
25impl<T: crate::Time> Tracker<T> {
26 /// Create a tracker that is full.
27 ///
28 /// This creates a tracker that has it's full burst capacity available.
29 pub fn full() -> Self {
30 Tracker::Full { overfull: 0 }
31 }
32
33 /// Create a tracker that is overfull.
34 ///
35 /// This tracker is not only full to its full burst capacity but also has extra capacity. This extra capacity will be consumed before dipping into the `burst` quota and regular rate limiting. This tracker will not fill itself up to the overfull state again, once the extra capacity is consumed it behaves as a regular tracker. (If you want it to fill further use a different `crate::Config` which has a larger burst capacity.)
36 ///
37 /// Note: This allows for requests up to [`crate::Config::burst + capacity`](Config) to be fulfilled until the overfull capacity runs out.
38 pub fn overfull(capacity: u32) -> Self {
39 Tracker::Full { overfull: capacity }
40 }
41
42 /// Create a tracker that is unlimited.
43 ///
44 /// This can be used to disable rate limiting for a specific tracker. It is semantically identical to being overfull with infinite capacity.
45 pub fn unlimited() -> Self {
46 Tracker::Unlimited
47 }
48
49 /// Create a tracker that starts at the provided time.
50 ///
51 /// This is an alias for [`Tracker::empty_at()`](Tracker::empty_at()).
52 #[deprecated(note="Use `empty_at()` which has a more descriptive name.")]
53 pub fn new_at(now: T) -> Self {
54 Self::empty_at(now)
55 }
56
57 /// Create a tracker that was empty at the provided time.
58 ///
59 /// This means that the bucket will be empty at the indicated time and will fill starting from there. Note that this is **not** what you want for an limit that hasn't been used for a while and cleaned from the DB. For that case you want [`Tracker::default()`].
60 ///
61 /// Note that if `now` is in the future it is possible to create a tracker that is empty and will remain empty until that point. This may be seen as a sort of "overdrawn" bucket that will take filling to return to zero.
62 pub fn empty_at(now: T) -> Self {
63 Tracker::Filling { empty_at: now }
64 }
65
66 /// Create a tracker that has the specified capacity at the specified time.
67 pub fn with_capacity_at(
68 config: &crate::Config<T>,
69 capacity: u32,
70 now: T
71 ) -> Self {
72 if let Some(overfull) = capacity.checked_sub(config.burst) {
73 Self::overfull(overfull)
74 } else {
75 Self::with_partial_capacity_at(config, capacity, now)
76 }
77 }
78
79 /// Create a tracker that has the specified capacity up to the burst size.
80 ///
81 /// This will never [overfill](Tracker::overfull()) the tracker, instead saturating at the burst capacity. If you want to allow overfilling use [`Tracker::with_capacity_at()`](Tracker::with_capacity_at()).
82 pub fn with_limited_capacity_at(
83 config: &crate::Config<T>,
84 capacity: u32,
85 now: T
86 ) -> Self {
87 if capacity >= config.burst {
88 Self::full()
89 } else {
90 Self::with_partial_capacity_at(config, capacity, now)
91 }
92 }
93
94 fn with_partial_capacity_at(
95 config: &crate::Config<T>,
96 capacity: u32,
97 now: T
98 ) -> Self {
99 debug_assert!(capacity <= config.burst);
100
101 Tracker::Filling {
102 empty_at: now.sub(config.rate.clone().mul(capacity)),
103 }
104 }
105
106 fn overfull_capacity(&self) -> u32 {
107 match *self {
108 Tracker::Full{overfull} => overfull,
109 Tracker::Filling{..} => 0,
110 Tracker::Unlimited => u32::MAX,
111 }
112 }
113
114 /// Returns the tokens available at the specified time.
115 ///
116 /// Note that history is not stored, this is just extrapolating from the current state. So if `config.rate` is 1/s advancing `now` by 10s will raise the capacity by 10 (up to `config.burst`), moving `now` back 10s will lower the capacity by 10 (down to 0). Moving `now` past the time where tokens were acquired will not restore those tokens to the capacity as returned by this function.
117 pub fn capacity_at(
118 &self,
119 config: &crate::Config<T>,
120 now: T,
121 ) -> u32 {
122 debug_assert_ne!(config.rate, T::Duration::zero(), "rate must be greater than zero");
123
124 match *self {
125 Tracker::Filling{ref empty_at} => {
126 match now.since(empty_at.clone()) {
127 Some(elapsed) => {
128 let periods = elapsed.div(config.rate.clone());
129 periods.min(config.burst)
130 }
131 None => 0,
132 }
133 }
134 Tracker::Full{overfull} => {
135 config.burst.saturating_add(overfull)
136 }
137 Tracker::Unlimited => u32::MAX,
138 }
139 }
140
141 /// Attempt to acquire `count` tokens from the rate limiter.
142 ///
143 /// If the requested number of tokens are available the state is updated and `Ok(())` is returned. Otherwise an error describing the issue is returned.
144 ///
145 /// Warning: To save memory a Tracker does not remember its crate::Config. This means that you can switch the `config` argument between calls on the same Tracker. This is **not recommended** as it is not immediately obvious what the result will be but is completely supported. The only guarantee provided is that the new rate limit will (eventually) take effect. The logical state of the Tracker when this occurs may be surprising. For example a large "burst" config may be immediately filled or a new low "rate" may result in a previous "burst" capacity disappearing. The exact behaviour is not guaranteed to be stable. If you want a specific behaviour it is best to extract the information you want with the old config and create a fresh tracker using the constructor functions to produce a tracker in the state that you expect.
146 pub fn acquire_at(
147 &mut self,
148 config: &crate::Config<T>,
149 count: u32,
150 now: T,
151 ) -> Result<(), crate::Denied<T>> {
152 self.acquire_range_at(config, count..=count, now)
153 .map(|acquired| {
154 debug_assert_eq!(acquired, count);
155 })
156 }
157
158 /// Acquire tokens from the tracker.
159 ///
160 /// Acquires at least `request.start()` tokens but no more than `request.end()`.
161 ///
162 /// Returns the number of tokens acquired.
163 ///
164 /// If the available capacity is less than `request.start()` `Err(crate::Denied::TooEarly)` is returned.
165 ///
166 /// If `request.start()` is zero than this call always succeeds.
167 pub fn acquire_range_at(
168 &mut self,
169 config: &crate::Config<T>,
170 request: std::ops::RangeInclusive<u32>,
171 now: T,
172 ) -> Result<u32, crate::Denied<T>> {
173 debug_assert_ne!(config.rate, T::Duration::zero(), "rate must be greater than zero");
174
175 if request.is_empty() {
176 return Err(crate::Denied::EmptyRequest)
177 }
178 if request.start().saturating_sub(self.overfull_capacity()) > config.burst {
179 return Err(crate::Denied::TooBig)
180 }
181
182 match *self {
183 Tracker::Filling{ref mut empty_at} => {
184 let mut fill_time = now.clone().since(empty_at.clone())
185 .unwrap_or(T::Duration::zero());
186
187 let max_fill_time = config.fill_time();
188 if fill_time > max_fill_time {
189 fill_time = max_fill_time.clone();
190 *empty_at = now.sub(max_fill_time);
191 }
192
193 let min_duration = config.rate.clone().mul(*request.start());
194 let max_duration = config.rate.clone().mul(*request.end());
195 if fill_time >= max_duration {
196 empty_at.add_assign(max_duration);
197 Ok(*request.end())
198 } else if fill_time < min_duration {
199 let min_available_at = empty_at.clone().add(min_duration);
200 Err(crate::Denied::TooEarly(crate::TooEarly{next: min_available_at}))
201 } else {
202 let capacity = fill_time.div(config.rate.clone());
203 empty_at.add_assign(config.rate.clone().mul(capacity));
204 Ok(capacity)
205 }
206 }
207 Tracker::Full{ref mut overfull} => {
208 if *overfull >= *request.end() {
209 *overfull -= request.end();
210 Ok(*request.end())
211 } else {
212 let total = overfull.saturating_add(config.burst);
213 let acquired = total.min(*request.end());
214 let remaining = total - acquired;
215 *self = Tracker::Filling {
216 empty_at: now.sub(config.rate.clone().mul(remaining)),
217 };
218 Ok(acquired)
219 }
220 }
221 Tracker::Unlimited => Ok(*request.end())
222 }
223 }
224
225 /// Acquire up to `count` tokens from the rate limiter.
226 ///
227 /// Returns the number of tokens actually acquired. Fails if no tokens were acquired. If 0 is acceptable use `.unwrap_or(0)` to explicitly waive the error.
228 ///
229 /// As a special case requesting `0` will always succeed with `Ok(0)`.
230 ///
231 /// WARNING: This function will return `Ok(0)` on disabled configs. If you are using disabled configs please migrate to [`Tracker::acquire_up_to_at_2()`][Tacker::acquire_up_to_at_2()] for a proper error.
232 ///
233 /// Equivalent to [`Tracker::acquire_range_at(config, 1..=count, now)`](Tracker::acquire_range_at()).
234 #[deprecated(note="Use acquire_up_to_at_2.")]
235 pub fn acquire_up_to_at(
236 &mut self,
237 config: &crate::Config<T>,
238 count: u32,
239 now: T,
240 ) -> Result<u32, crate::TooEarly<T>> {
241 match self.acquire_up_to_at_2(config, count, now) {
242 Ok(acquired) => Ok(acquired),
243 Err(crate::Denied::TooEarly(err)) => {
244 Err(err)
245 }
246 Err(crate::Denied::TooBig) if config.is_disabled() => {
247 Ok(0)
248 }
249 Err(other) => {
250 unreachable!("Unexpected error in acquire_up_to(): {}", other)
251 }
252 }
253 }
254
255 /// Acquire up to `count` tokens from the rate limiter.
256 ///
257 /// Returns the number of tokens actually acquired. Fails if no tokens were acquired. If 0 is acceptable use `.unwrap_or(0)` to explicitly waive the error.
258 ///
259 /// As a special case requesting `0` will always succeed with `Ok(0)`.
260 ///
261 /// Equivalent to [`Tracker::acquire_range_at(config, 1..=count, now)`](Tracker::acquire_range_at()).
262 pub fn acquire_up_to_at_2(
263 &mut self,
264 config: &crate::Config<T>,
265 count: u32,
266 now: T,
267 ) -> Result<u32, crate::Denied<T>> {
268 if count == 0 {
269 return Ok(0)
270 }
271
272 self.acquire_range_at(config, 1..=count, now)
273 }
274
275 /// Force acquire from the rate limiter.
276 ///
277 /// This can not fail, the capacity can go "below zero" using this method. For example if the Config replenishes at 1 token per second and there are 2 tokens available, then acquiring 5 tokens will result in a capacity of 0 but requires 4 seconds for the next token to become available.
278 ///
279 /// Note: While [`Tracker::capacity()`](Tracker::capacity()) will never report negative values the time reported in [`TooEarly.available_at()`](TooEarly.available_at()) will accurately reflect when the request will become available.
280 pub fn force_acquire_at(
281 &mut self,
282 config: &crate::Config<T>,
283 count: u32,
284 now: T,
285 ) {
286 match self {
287 Tracker::Filling { empty_at } => {
288 empty_at.add_assign(config.rate.clone().mul(count));
289 }
290 Tracker::Full { overfull } => {
291 if *overfull >= count {
292 *overfull -= count;
293 } else {
294 let remaining = count - *overfull;
295 *self = Tracker::Filling {
296 empty_at: now
297 .add(config.rate.clone().mul(remaining))
298 .sub(config.fill_time()),
299 }
300 }
301 }
302 Tracker::Unlimited => {}
303 }
304 }
305
306 /// Add capacity.
307 ///
308 /// This adds some number of tokens to the tracker.
309 ///
310 /// Warning: This method will overfill the tracker. If this is not desired use [`add_limited_capacity_at()`][Tracker::add_limited_capacity_at()].
311 ///
312 /// Note: When this method overfills the tracker any existing progress toward the next token is lost. For example if a 1s token interval was 1ns away from fully filling, adding 1 capacity will effectively add that 1ns of time, whereas if it was 999ms away from full it will add 999ms. This also means that `t.add_capacity_at(c, 1, now); t.acquire_at(c, 1, now)` will not always leave `t` in the same state as if the additional fills the tracker the incremental progress will be reset.
313 pub fn add_capacity_at(
314 &mut self,
315 config: &crate::Config<T>,
316 tokens: u32,
317 now: T,
318 ) {
319 match self {
320 Tracker::Filling { empty_at } => {
321 let min = now.clone().sub(config.fill_time());
322 if *empty_at < min {
323 *empty_at = min.clone()
324 }
325 let duration = config.rate.clone().mul(tokens);
326 empty_at.sub_assign(duration);
327 if let Some(overfull_duration) = min.clone().since(empty_at.clone()) {
328 *self = Tracker::overfull(overfull_duration.div(config.rate.clone()));
329 }
330 }
331 Tracker::Full{overfull} => {
332 *overfull = overfull.saturating_add(tokens);
333 }
334 Tracker::Unlimited => {}
335 }
336 }
337
338 /// Add capacity without overfilling.
339 ///
340 /// This adds some number of tokens to the tracker without allowing it to overfill.
341 ///
342 /// Note: If overfilling is desired use [`add_capacity()`][Tracker::add_capacity_at()].
343 pub fn add_limited_capacity_at(
344 &mut self,
345 config: &crate::Config<T>,
346 tokens: u32,
347 _now: T,
348 ) {
349 match self {
350 Tracker::Filling { empty_at } => {
351 let duration = config.rate.clone().mul(tokens);
352 empty_at.sub_assign(duration);
353 }
354 Tracker::Full{..} => {}
355 Tracker::Unlimited => {}
356 }
357 }
358
359 /// Attempts to minimize the state.
360 ///
361 /// If a rate limit hasn't been used in a long time the bucket will be full and the state can be simplified.
362 ///
363 /// Returns `true` if the state is "simple" and equivalent to [`Tracker::default()`]. If this occurs you don't need to store the state and can use the default state if needed again.
364 pub fn simplify_at(
365 &mut self,
366 config: &crate::Config<T>,
367 now: T,
368 ) -> bool {
369 if let Tracker::Filling{empty_at} = self {
370 let min = now.sub(config.fill_time());
371 if *empty_at <= min || config.is_disabled() {
372 *self = Tracker::full();
373 }
374 }
375
376 matches!(self, Tracker::Full { overfull: 0 })
377 }
378}