throttle_ro/
lib.rs

1//! A throttling service that limits the number of attempts (hits) from an IP address
2//! within a specified time period.
3//!
4//! # Example: Basic Rate Limiting
5//!
6//! ```
7//! use std::time::Duration;
8//! use cache_ro::Cache;
9//! use throttle_ro::ThrottlesService;
10//!
11//! // Create a cache instance (in-memory for this example)
12//! let cache = Cache::new(cache_ro::CacheConfig {
13//!     persistent: false,
14//!     ..Default::default()
15//! }).unwrap();
16//!
17//! // Create a throttling service for IP "127.0.0.1"
18//! // allowing maximum 5 attempts per minute
19//! let ip = "127.0.0.1".to_string();
20//! let mut service = ThrottlesService::new(
21//!     ip,
22//!     5, // max attempts
23//!     Duration::from_secs(60), // time window
24//!     "api_rate_limit_" // cache key prefix
25//! );
26//!
27//! // Check if the IP is allowed to proceed
28//! if service.can_go(&cache) {
29//!     // Record the attempt
30//!     service.hit(&cache);
31//!     println!("Request allowed");
32//!     // Process the request...
33//! } else {
34//!     println!("Rate limit exceeded - please try again later");
35//!     // Return error or wait...
36//! }
37//!
38//! // You can also manually clear the throttle if needed
39//! // service.remove(&cache);
40//! ```
41//!
42
43
44use cache_ro::Cache;
45use std::time::Duration;
46
47/// A service for throttling attempts from an IP address.
48///
49/// Tracks the number of attempts (hits) from a given IP address and determines
50/// whether further attempts should be allowed based on configured limits.
51///
52/// # Examples
53///
54/// ```
55/// use std::time::Duration;
56/// use cache_ro::Cache;
57/// use throttle_ro::ThrottlesService;
58///
59/// let cache = Cache::new(Default::default()); // In real usage, configure properly
60/// let ip = "127.0.0.1".to_string();
61/// let mut service = ThrottlesService::new(
62///     ip,
63///     5, // max attempts
64///     Duration::from_secs(60), // time window
65///     "rate_limit_"
66/// );
67///
68/// if service.can_go(&cache) {
69///     service.hit(&cache);
70///     // Process the request
71/// } else {
72///     // Reject the request - rate limit exceeded
73/// }
74/// ```
75pub struct ThrottlesService {
76    ip: String,
77    max_attempts: u32,
78    period: Duration,
79    prefix: String,
80}
81
82impl ThrottlesService {
83    /// Creates a new `ThrottlesService` instance.
84    ///
85    /// # Arguments
86    ///
87    /// * `ip` - The IP address to track
88    /// * `max_attempts` - Maximum number of allowed attempts in the time period
89    /// * `period` - Duration of the throttling window
90    /// * `prefix` - Prefix for cache keys to avoid collisions
91    pub fn new(ip: String, max_attempts: u32, period: Duration, prefix: &str) -> Self {
92        Self {
93            ip,
94            max_attempts,
95            period,
96            prefix: prefix.to_string(),
97        }
98    }
99
100    /// Checks whether the IP is allowed to make another attempt.
101    ///
102    /// Returns `true` if the current attempt count is below the maximum allowed.
103    pub fn can_go(&mut self, cache: &Cache) -> bool {
104        let v = self.get_value(cache).unwrap_or(0);
105        v < self.max_attempts
106    }
107
108    fn get_value(&mut self, cache: &Cache) -> Option<u32> {
109        cache.get::<u32>(&self.key())
110    }
111
112    /// Generates the cache key for this IP.
113    pub fn key(&self) -> String {
114        format!("{}{}", self.prefix, self.ip)
115    }
116
117    /// Gets the remaining duration for the current throttling window.
118    ///
119    /// Returns the configured period if no expiration is set in the cache.
120    pub fn get_expire(&mut self, cache: &Cache) -> Duration {
121        let ex = cache.expire(&self.key());
122        match ex {
123            None => self.period,
124            Some(a) => a,
125        }
126    }
127
128    /// Records an attempt (hit) from the IP.
129    ///
130    /// Increments the attempt count and resets the expiration time.
131    pub fn hit(&mut self, cache: &Cache) {
132        let key = self.key();
133        let expire = self.get_expire(cache);
134
135        match self.get_value(cache) {
136            None => cache.set::<u32>(&key, 1, expire).unwrap(),
137            Some(v) => cache.set::<u32>(&key, v + 1, expire).unwrap(),
138        }
139    }
140
141    /// Clears the attempt count for the IP.
142    pub fn remove(&self, cache: &Cache) {
143        cache.remove(&self.key()).unwrap();
144    }
145}