rusty_feeder/limit/helpers.rs
1//! Common helpers for rate limiter implementations
2//!
3//! This module provides shared utility functions for timestamp cleanup, token refill calculations,
4//! and other common rate limiting operations to reduce code duplication across different
5//! rate limiter implementations.
6
7use std::time::{Duration, Instant};
8
9/// Calculate the number of tokens to refill based on elapsed time
10///
11/// This function implements the standard proportional token refill algorithm
12/// used across all rate limiter implementations. It calculates how many tokens
13/// should be available based on the time elapsed since the last refresh.
14///
15/// # Arguments
16/// * `elapsed` - Time elapsed since last refresh
17/// * `window_duration` - Duration of the rate limiting window
18/// * `max_tokens` - Maximum number of tokens in the bucket
19/// * `current_tokens` - Current number of available tokens
20///
21/// # Returns
22/// The new number of available tokens (capped at max_tokens)
23#[inline]
24pub fn calculate_token_refill(
25 elapsed: Duration,
26 window_duration: Duration,
27 max_tokens: usize,
28 current_tokens: usize,
29) -> usize {
30 if elapsed >= window_duration {
31 // Full window has passed, reset to maximum
32 max_tokens
33 } else {
34 // Partial refill based on elapsed time
35 let refill_ratio = elapsed.as_secs_f64() / window_duration.as_secs_f64();
36 let tokens_to_add = (refill_ratio * max_tokens as f64) as usize;
37 (current_tokens + tokens_to_add).min(max_tokens)
38 }
39}
40
41/// Calculate the proportional time adjustment for partial token refill
42///
43/// When tokens are partially refilled, the last refresh time needs to be adjusted
44/// proportionally to maintain accurate rate limiting.
45///
46/// # Arguments
47/// * `last_refresh` - The last refresh time
48/// * `now` - Current time
49/// * `elapsed` - Time elapsed since last refresh
50/// * `window_duration` - Duration of the rate limiting window
51/// * `tokens_added` - Number of tokens that were added
52/// * `max_tokens` - Maximum number of tokens in the bucket
53///
54/// # Returns
55/// The adjusted last refresh time
56#[inline]
57pub fn calculate_proportional_time_adjustment(
58 last_refresh: Instant,
59 now: Instant,
60 elapsed: Duration,
61 window_duration: Duration,
62 tokens_added: usize,
63 max_tokens: usize,
64) -> Instant {
65 if tokens_added == 0 || max_tokens == 0 {
66 return last_refresh;
67 }
68
69 // Calculate the time represented by the tokens added
70 let token_ratio = tokens_added as f64 / max_tokens as f64;
71 let time_for_tokens = Duration::from_secs_f64(window_duration.as_secs_f64() * token_ratio);
72
73 // Adjust the last refresh time forward by the time represented by tokens added
74 last_refresh + time_for_tokens
75}
76
77/// Clean up timestamps older than the specified cutoff time
78///
79/// This function removes all timestamps from a collection that are older than
80/// the cutoff time, helping to prevent unbounded memory growth in rate limiters
81/// that track individual request timestamps.
82///
83/// # Arguments
84/// * `timestamps` - Mutable reference to a vector of timestamps
85/// * `cutoff` - The cutoff instant; timestamps older than this will be removed
86///
87/// # Returns
88/// The number of timestamps that were removed
89#[inline]
90pub fn cleanup_old_timestamps(timestamps: &mut Vec<Instant>, cutoff: Instant) -> usize {
91 let initial_len = timestamps.len();
92
93 // Remove timestamps older than cutoff
94 timestamps.retain(|×tamp| timestamp >= cutoff);
95
96 initial_len - timestamps.len()
97}
98
99/// Clean up timestamps older than the specified duration from now
100///
101/// Convenience function that calculates the cutoff time based on the current time
102/// and a duration, then calls cleanup_old_timestamps.
103///
104/// # Arguments
105/// * `timestamps` - Mutable reference to a vector of timestamps
106/// * `window_duration` - Duration of the rate limiting window
107/// * `now` - Current time instant
108///
109/// # Returns
110/// The number of timestamps that were removed
111#[inline]
112pub fn cleanup_timestamps_by_duration(
113 timestamps: &mut Vec<Instant>,
114 window_duration: Duration,
115 now: Instant,
116) -> usize {
117 let cutoff = now - window_duration;
118 cleanup_old_timestamps(timestamps, cutoff)
119}
120
121/// Record a new timestamp and perform cleanup if needed
122///
123/// This function adds a new timestamp to the collection and optionally performs
124/// cleanup of old timestamps to maintain memory efficiency.
125///
126/// # Arguments
127/// * `timestamps` - Mutable reference to a vector of timestamps
128/// * `timestamp` - The new timestamp to record
129/// * `window_duration` - Duration of the rate limiting window
130/// * `max_capacity` - Optional maximum capacity for the timestamp vector
131/// * `auto_cleanup` - Whether to automatically cleanup old timestamps
132///
133/// # Returns
134/// The number of timestamps that were cleaned up (if any)
135#[inline]
136pub fn record_timestamp_with_cleanup(
137 timestamps: &mut Vec<Instant>,
138 timestamp: Instant,
139 window_duration: Duration,
140 max_capacity: Option<usize>,
141 auto_cleanup: bool,
142) -> usize {
143 timestamps.push(timestamp);
144
145 let mut cleaned_count = 0;
146
147 // Perform cleanup if requested
148 if auto_cleanup {
149 cleaned_count += cleanup_timestamps_by_duration(timestamps, window_duration, timestamp);
150 }
151
152 // Enforce capacity limit if specified
153 if let Some(max_cap) = max_capacity
154 && timestamps.len() > max_cap
155 {
156 let excess = timestamps.len() - max_cap;
157 timestamps.drain(0..excess);
158 cleaned_count += excess;
159 }
160
161 cleaned_count
162}
163
164/// Check if enough time has passed for a full window reset
165///
166/// This is a common check across rate limiters to determine if the window
167/// has completely elapsed and tokens should be reset to maximum.
168///
169/// # Arguments
170/// * `elapsed` - Time elapsed since last refresh
171/// * `window_duration` - Duration of the rate limiting window
172///
173/// # Returns
174/// True if a full window reset should occur
175#[inline]
176pub fn should_perform_full_reset(elapsed: Duration, window_duration: Duration) -> bool {
177 elapsed >= window_duration
178}
179
180/// Calculate token refill for nanosecond-based rate limiters
181///
182/// Specialized version for rate limiters that work with nanosecond timestamps
183/// instead of Duration/Instant types.
184///
185/// # Arguments
186/// * `elapsed_ns` - Nanoseconds elapsed since last refresh
187/// * `window_ns` - Duration of the rate limiting window in nanoseconds
188/// * `max_tokens` - Maximum number of tokens in the bucket
189/// * `current_tokens` - Current number of available tokens
190///
191/// # Returns
192/// The new number of available tokens (capped at max_tokens)
193#[inline]
194pub fn calculate_token_refill_ns(
195 elapsed_ns: u64,
196 window_ns: u64,
197 max_tokens: usize,
198 current_tokens: usize,
199) -> usize {
200 if elapsed_ns >= window_ns {
201 // Full window has passed, reset to maximum
202 max_tokens
203 } else if elapsed_ns > 0 {
204 // Partial refill based on elapsed time
205 let refill_ratio = elapsed_ns as f64 / window_ns as f64;
206 let tokens_to_add = (refill_ratio * max_tokens as f64) as usize;
207 (current_tokens + tokens_to_add).min(max_tokens)
208 } else {
209 current_tokens
210 }
211}
212
213/// Calculate proportional time adjustment for nanosecond-based rate limiters
214///
215/// # Arguments
216/// * `last_refill_ns` - The last refresh time in nanoseconds
217/// * `elapsed_ns` - Nanoseconds elapsed since last refresh
218/// * `window_ns` - Duration of the rate limiting window in nanoseconds
219/// * `tokens_added` - Number of tokens that were added
220/// * `max_tokens` - Maximum number of tokens in the bucket
221///
222/// # Returns
223/// The adjusted last refresh time in nanoseconds
224#[inline]
225pub const fn calculate_proportional_time_adjustment_ns(
226 last_refill_ns: u64,
227 elapsed_ns: u64,
228 window_ns: u64,
229 tokens_added: usize,
230 max_tokens: usize,
231) -> u64 {
232 if tokens_added == 0 || max_tokens == 0 {
233 return last_refill_ns;
234 }
235
236 // Calculate the time represented by the tokens added
237 let time_for_tokens = (elapsed_ns * tokens_added as u64) / max_tokens as u64;
238
239 // Adjust the last refresh time forward by the time represented by tokens added
240 last_refill_ns + time_for_tokens
241}
242
243/// Check if enough time has passed for a full window reset (nanosecond version)
244///
245/// # Arguments
246/// * `elapsed_ns` - Nanoseconds elapsed since last refresh
247/// * `window_ns` - Duration of the rate limiting window in nanoseconds
248///
249/// # Returns
250/// True if a full window reset should occur
251#[inline]
252pub const fn should_perform_full_reset_ns(elapsed_ns: u64, window_ns: u64) -> bool {
253 elapsed_ns >= window_ns
254}
255
256/// Calculate tokens available after time-based refill
257///
258/// Higher-level function that combines token calculation and proportional time adjustment.
259/// This is the main function used by rate limiters to update their state.
260///
261/// # Arguments
262/// * `current_tokens` - Current number of available tokens
263/// * `max_tokens` - Maximum number of tokens in the bucket
264/// * `last_refresh` - The last refresh time
265/// * `now` - Current time
266/// * `window_duration` - Duration of the rate limiting window
267///
268/// # Returns
269/// A tuple of (new_token_count, new_last_refresh_time)
270#[inline]
271pub fn refill_tokens_with_time_adjustment(
272 current_tokens: usize,
273 max_tokens: usize,
274 last_refresh: Instant,
275 now: Instant,
276 window_duration: Duration,
277) -> (usize, Instant) {
278 let elapsed = now.saturating_duration_since(last_refresh);
279
280 if should_perform_full_reset(elapsed, window_duration) {
281 // Full reset
282 (max_tokens, now)
283 } else {
284 // Partial refill
285 let new_tokens =
286 calculate_token_refill(elapsed, window_duration, max_tokens, current_tokens);
287 let tokens_added = new_tokens.saturating_sub(current_tokens);
288
289 let new_last_refresh = if tokens_added > 0 {
290 calculate_proportional_time_adjustment(
291 last_refresh,
292 now,
293 elapsed,
294 window_duration,
295 tokens_added,
296 max_tokens,
297 )
298 } else {
299 last_refresh
300 };
301
302 (new_tokens, new_last_refresh)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_calculate_token_refill_full_window() {
312 let window = Duration::from_secs(1);
313 let elapsed = Duration::from_secs(2); // More than full window
314 let max_tokens = 100;
315 let current_tokens = 50;
316
317 let result = calculate_token_refill(elapsed, window, max_tokens, current_tokens);
318 assert_eq!(result, max_tokens);
319 }
320
321 #[test]
322 fn test_calculate_token_refill_partial() {
323 let window = Duration::from_secs(1);
324 let elapsed = Duration::from_millis(500); // Half window
325 let max_tokens = 100;
326 let current_tokens = 0;
327
328 let result = calculate_token_refill(elapsed, window, max_tokens, current_tokens);
329 assert_eq!(result, 50); // Half the tokens
330 }
331
332 #[test]
333 fn test_calculate_token_refill_cap_at_max() {
334 let window = Duration::from_secs(1);
335 let elapsed = Duration::from_millis(500); // Half window
336 let max_tokens = 100;
337 let current_tokens = 80; // Already near max
338
339 let result = calculate_token_refill(elapsed, window, max_tokens, current_tokens);
340 assert_eq!(result, max_tokens); // Capped at max
341 }
342
343 #[test]
344 fn test_cleanup_old_timestamps() {
345 let now = Instant::now();
346 let old_time = now - Duration::from_secs(2);
347 let recent_time = now - Duration::from_millis(500);
348
349 let mut timestamps = vec![old_time, recent_time, now];
350 let cutoff = now - Duration::from_secs(1);
351
352 let removed_count = cleanup_old_timestamps(&mut timestamps, cutoff);
353
354 assert_eq!(removed_count, 1);
355 assert_eq!(timestamps.len(), 2);
356 assert!(!timestamps.contains(&old_time));
357 assert!(timestamps.contains(&recent_time));
358 assert!(timestamps.contains(&now));
359 }
360
361 #[test]
362 fn test_record_timestamp_with_cleanup() {
363 let now = Instant::now();
364 let old_time = now - Duration::from_secs(2);
365
366 let mut timestamps = vec![old_time];
367 let window = Duration::from_secs(1);
368
369 let cleaned_count = record_timestamp_with_cleanup(&mut timestamps, now, window, None, true);
370
371 assert_eq!(cleaned_count, 1); // Old timestamp was cleaned
372 assert_eq!(timestamps.len(), 1);
373 assert!(timestamps.contains(&now));
374 assert!(!timestamps.contains(&old_time));
375 }
376
377 #[test]
378 fn test_refill_tokens_with_time_adjustment_full_reset() {
379 let now = Instant::now();
380 let last_refresh = now - Duration::from_secs(2);
381 let window = Duration::from_secs(1);
382 let max_tokens = 100;
383 let current_tokens = 50;
384
385 let (new_tokens, new_last_refresh) = refill_tokens_with_time_adjustment(
386 current_tokens,
387 max_tokens,
388 last_refresh,
389 now,
390 window,
391 );
392
393 assert_eq!(new_tokens, max_tokens);
394 assert_eq!(new_last_refresh, now);
395 }
396
397 #[test]
398 fn test_refill_tokens_with_time_adjustment_partial() {
399 let now = Instant::now();
400 let last_refresh = now - Duration::from_millis(500);
401 let window = Duration::from_secs(1);
402 let max_tokens = 100;
403 let current_tokens = 0;
404
405 let (new_tokens, _new_last_refresh) = refill_tokens_with_time_adjustment(
406 current_tokens,
407 max_tokens,
408 last_refresh,
409 now,
410 window,
411 );
412
413 assert_eq!(new_tokens, 50); // Half the tokens for half the time
414 }
415}