-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
251 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
use tokio::time::{Duration, Instant}; | ||
|
||
/// Token bucket rate limiter. | ||
pub struct RateLimiter { | ||
burst_capacity: f64, // Maximum number of tokens | ||
tokens: f64, // Current number of tokens (can be negative) | ||
refill_rate_per_sec: f64, // Tokens added per second | ||
last_refill: Instant, // Last time tokens were refilled | ||
max_negative_tokens: f64, // Maximum allowed negative tokens (deficit) | ||
} | ||
|
||
impl RateLimiter { | ||
/// Creates a new RateLimiter. | ||
pub fn new(burst_capacity: f64, refill_duration: Duration) -> Self { | ||
let refill_rate_per_sec = burst_capacity / refill_duration.as_secs_f64(); | ||
let tokens = burst_capacity; | ||
let max_negative_tokens = burst_capacity * 1000.0; | ||
|
||
Self { | ||
burst_capacity, | ||
tokens, | ||
refill_rate_per_sec, | ||
last_refill: Instant::now(), | ||
max_negative_tokens, | ||
} | ||
} | ||
|
||
/// Refills tokens based on the elapsed time since the last refill. | ||
fn refill_tokens(&mut self) { | ||
let now = Instant::now(); | ||
let elapsed = now.duration_since(self.last_refill).as_secs_f64(); | ||
self.last_refill = now; | ||
|
||
let tokens_to_add = elapsed * self.refill_rate_per_sec; | ||
self.tokens = (self.tokens + tokens_to_add).min(self.burst_capacity); | ||
} | ||
|
||
/// Checks if the specified number of tokens are available without consuming them. | ||
pub fn can_consume(&mut self, tokens_needed: f64) -> bool { | ||
self.refill_tokens(); | ||
self.tokens >= tokens_needed | ||
} | ||
|
||
/// Attempts to consume the specified number of tokens. | ||
pub fn consume(&mut self, tokens_needed: f64) -> bool { | ||
self.refill_tokens(); | ||
if self.tokens >= tokens_needed { | ||
self.tokens -= tokens_needed; | ||
true | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
/// Consumes tokens regardless of availability (can result in negative token count). | ||
pub fn overcharge(&mut self, tokens_needed: f64) { | ||
self.refill_tokens(); | ||
self.tokens -= tokens_needed; | ||
|
||
if self.tokens < -self.max_negative_tokens { | ||
self.tokens = -self.max_negative_tokens; | ||
} | ||
} | ||
|
||
pub fn get_available_tokens(&mut self) -> f64 { | ||
self.refill_tokens(); | ||
self.tokens | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use tokio::time::{self, advance}; | ||
use tokio::time::{Duration, Instant}; | ||
|
||
#[tokio::test] | ||
async fn test_initial_tokens() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(86400); // 1 day | ||
let rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
assert_eq!(rate_limiter.tokens, capacity); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_consume_tokens_success() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(86400); // 1 day | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Consume 5 tokens | ||
let result = rate_limiter.consume(5.0); | ||
assert!(result); | ||
assert_eq!(rate_limiter.tokens, 5.0); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_consume_tokens_failure() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(86400); // 1 day | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Attempt to consume more tokens than available | ||
let result = rate_limiter.consume(15.0); | ||
assert!(!result); | ||
// Tokens should remain unchanged since consume failed | ||
assert_eq!(rate_limiter.tokens, capacity); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_overcharge() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(86400); // 1 day | ||
let max_negative_tokens = capacity * 1000.0; | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Overcharge by 15 tokens | ||
rate_limiter.overcharge(15.0); | ||
assert_eq!(rate_limiter.tokens, -5.0); | ||
|
||
// Overcharge beyond max_negative_tokens | ||
rate_limiter.overcharge(max_negative_tokens * 2.0); | ||
assert_eq!(rate_limiter.tokens, -max_negative_tokens); | ||
} | ||
|
||
#[tokio::test(start_paused = true)] | ||
async fn test_refill_tokens() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(10); // Short duration for testing | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Consume all tokens | ||
let result = rate_limiter.consume(capacity); | ||
assert!(result); | ||
assert_eq!(rate_limiter.tokens, 0.0); | ||
|
||
// Advance time by half of the refill duration | ||
time::advance(Duration::from_secs(5)).await; | ||
rate_limiter.refill_tokens(); | ||
// Should have refilled half the tokens | ||
assert_eq!(rate_limiter.tokens, 5.0); | ||
|
||
// Advance time to complete the refill duration | ||
time::advance(Duration::from_secs(5)).await; | ||
rate_limiter.refill_tokens(); | ||
assert_eq!(rate_limiter.tokens, capacity); | ||
} | ||
|
||
#[tokio::test(start_paused = true)] | ||
async fn test_overcharge_and_refill() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(10); // Short duration for testing | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Overcharge by 15 tokens | ||
rate_limiter.overcharge(15.0); | ||
assert_eq!(rate_limiter.tokens, -5.0); | ||
|
||
// Advance time to refill tokens | ||
time::advance(Duration::from_secs(20)).await; // Wait enough to refill capacity | ||
rate_limiter.refill_tokens(); | ||
// Tokens should be at capacity, but deficit should be reduced | ||
assert_eq!(rate_limiter.tokens, capacity); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_max_negative_tokens() { | ||
let capacity = 10.0; | ||
let refill_duration = Duration::from_secs(86400); // 1 day | ||
let max_negative_tokens = capacity * 1000.0; | ||
let mut rate_limiter = RateLimiter::new(capacity, refill_duration); | ||
|
||
// Overcharge repeatedly to exceed max_negative_tokens | ||
rate_limiter.overcharge(max_negative_tokens + 50.0); | ||
assert_eq!(rate_limiter.tokens, -max_negative_tokens); | ||
} | ||
} |