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(|&timestamp| 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}