use std::time::Duration;
pub(crate) const RATE_FLOOR: f64 = 0.5;
pub(crate) const RAMP_AFTER: u32 = 10;
const DECREASE_FACTOR: f64 = 0.5;
const INCREASE_FACTOR: f64 = 1.25;
const CEILING_MARGIN: f64 = 0.9;
pub(crate) const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(5);
pub(crate) const MAX_RETRY_AFTER: Duration = Duration::from_secs(60);
pub(crate) struct AdaptiveLimiter {
rate: f64,
floor: f64,
ceiling: Option<f64>,
successes: u32,
throttled: bool,
}
impl AdaptiveLimiter {
pub(crate) fn new(initial_rate: f64) -> Self {
let initial_rate = if initial_rate.is_finite() && initial_rate > 0.0 {
initial_rate
} else {
RATE_FLOOR
};
let floor = RATE_FLOOR.min(initial_rate);
Self {
rate: initial_rate.max(floor),
floor,
ceiling: None,
successes: 0,
throttled: false,
}
}
pub(crate) fn pace(&mut self) -> Duration {
if !self.throttled {
return Duration::ZERO;
}
Duration::from_secs_f64(1.0 / self.rate)
}
#[cfg(test)]
pub(crate) fn rate(&self) -> f64 {
self.rate
}
pub(crate) fn on_success(&mut self) {
self.successes += 1;
if self.successes < RAMP_AFTER {
return;
}
let mut ramped = self.rate * INCREASE_FACTOR;
if let Some(ceiling) = self.ceiling {
ramped = ramped.min(ceiling * CEILING_MARGIN);
}
self.rate = ramped.max(self.floor);
self.successes = 0;
}
pub(crate) fn on_rate_limit(&mut self) {
self.throttled = true;
self.ceiling = Some(self.rate);
self.rate = (self.rate * DECREASE_FACTOR).max(self.floor);
self.successes = 0;
}
}
pub(crate) fn retry_after_delay(retry_after: Option<Duration>) -> Duration {
retry_after
.unwrap_or(DEFAULT_RETRY_AFTER)
.min(MAX_RETRY_AFTER)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pace_is_zero_until_the_first_rate_limit() {
let mut limiter = AdaptiveLimiter::new(2.0);
assert_eq!(limiter.pace(), Duration::ZERO);
for _ in 0..100 {
limiter.on_success();
assert_eq!(limiter.pace(), Duration::ZERO);
}
limiter.on_rate_limit();
assert!(limiter.pace() > Duration::ZERO);
}
#[test]
fn a_rate_limit_halves_the_rate_records_a_ceiling_and_engages_pacing() {
let mut limiter = AdaptiveLimiter::new(4.0);
limiter.on_rate_limit();
assert_eq!(limiter.rate(), 2.0);
assert_eq!(limiter.pace(), Duration::from_millis(500));
}
#[test]
fn the_rate_never_drops_below_the_floor() {
let mut limiter = AdaptiveLimiter::new(1.0);
for _ in 0..10 {
limiter.on_rate_limit();
}
assert_eq!(limiter.rate(), RATE_FLOOR);
assert_eq!(limiter.pace(), Duration::from_secs(2));
}
#[test]
fn ramps_up_only_after_ten_consecutive_successes() {
let mut limiter = AdaptiveLimiter::new(2.0);
for _ in 0..(RAMP_AFTER - 1) {
limiter.on_success();
}
assert_eq!(limiter.rate(), 2.0);
limiter.on_success();
assert!((limiter.rate() - 2.5).abs() < 1e-9);
}
#[test]
fn a_success_streak_resets_after_a_rate_limit() {
let mut limiter = AdaptiveLimiter::new(2.0);
for _ in 0..(RAMP_AFTER - 1) {
limiter.on_success();
}
limiter.on_rate_limit();
assert_eq!(limiter.rate(), 1.0);
for _ in 0..(RAMP_AFTER - 1) {
limiter.on_success();
}
assert_eq!(limiter.rate(), 1.0);
}
#[test]
fn a_ramp_is_capped_below_the_last_ceiling() {
let mut limiter = AdaptiveLimiter::new(4.0);
limiter.on_rate_limit();
assert_eq!(limiter.rate(), 2.0);
for _ in 0..(RAMP_AFTER * 20) {
limiter.on_success();
}
assert!((limiter.rate() - 3.6).abs() < 1e-9);
}
#[test]
fn retry_after_defaults_when_absent_and_caps_when_long() {
assert_eq!(retry_after_delay(None), DEFAULT_RETRY_AFTER);
assert_eq!(
retry_after_delay(Some(Duration::from_secs(7))),
Duration::from_secs(7)
);
assert_eq!(
retry_after_delay(Some(Duration::from_secs(600))),
MAX_RETRY_AFTER
);
}
}