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}