use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use parking_lot::Mutex;
pub struct RetryBudget {
max_tokens: u32,
window: Duration,
tokens: AtomicU32,
window_start: Mutex<Instant>,
}
impl RetryBudget {
pub fn new(max_tokens: u32, window: Duration) -> Arc<Self> {
Arc::new(Self {
max_tokens,
window,
tokens: AtomicU32::new(max_tokens),
window_start: Mutex::new(Instant::now()),
})
}
pub fn acquire(&self) -> bool {
self.maybe_refill();
let prev = self.tokens.fetch_sub(1, Ordering::SeqCst);
if prev > 0 {
true
} else {
self.tokens.fetch_add(1, Ordering::SeqCst);
false
}
}
pub fn available(&self) -> u32 {
self.maybe_refill();
self.tokens.load(Ordering::SeqCst)
}
fn maybe_refill(&self) {
let mut start = self.window_start.lock();
let elapsed = start.elapsed();
if elapsed >= self.window {
self.tokens.store(self.max_tokens, Ordering::SeqCst);
*start = Instant::now();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_exhaustion() {
let budget = RetryBudget::new(2, Duration::from_secs(10));
assert!(budget.acquire());
assert!(budget.acquire());
assert!(!budget.acquire());
assert_eq!(budget.available(), 0);
}
#[test]
fn window_refill() {
let budget = RetryBudget::new(1, Duration::from_millis(50));
assert!(budget.acquire());
assert!(!budget.acquire());
std::thread::sleep(Duration::from_millis(60));
assert!(budget.acquire());
assert!(!budget.acquire());
}
#[test]
fn concurrent_acquisition() {
use std::sync::Arc;
use std::thread;
let budget = RetryBudget::new(5, Duration::from_secs(10));
let budget = Arc::new(budget);
let handles: Vec<_> = (0..10)
.map(|_| {
let b = Arc::clone(&budget);
thread::spawn(move || b.acquire())
})
.collect();
let mut acquired = 0;
for h in handles {
if h.join().unwrap() {
acquired += 1;
}
}
assert_eq!(acquired, 5);
assert_eq!(budget.available(), 0);
}
}