use super::{EndpointRateLimit, RateLimitDecision};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Step {
Blocked { retry_after: f64 },
Allowed { count: f64, reset: bool },
}
impl From<Step> for Option<RateLimitDecision> {
fn from(step: Step) -> Self {
match step {
Step::Blocked { retry_after } => Some(RateLimitDecision::Blocked { retry_after }),
Step::Allowed { .. } => None,
}
}
}
#[must_use]
pub fn step(count: f64, last_request: i64, now: i64, rule: &EndpointRateLimit) -> Step {
let elapsed = chrono::Duration::milliseconds(now.saturating_sub(last_request))
.to_std()
.map_or(0.0, |duration| duration.as_secs_f64());
let expired = elapsed >= rule.window_seconds;
if !expired
&& (rule.window_seconds.is_nan()
|| rule.max_requests.is_nan()
|| count >= rule.max_requests)
{
return Step::Blocked {
retry_after: (rule.window_seconds - elapsed).ceil(),
};
}
Step::Allowed {
count: if expired { 1.0 } else { count + 1.0 },
reset: expired,
}
}
#[must_use]
pub fn expires_at(now: i64, window: f64) -> Option<i64> {
if window <= 0.0 || window.is_nan() {
return Some(now);
}
millis(window).and_then(|milliseconds| now.checked_add(milliseconds))
}
fn millis(seconds: f64) -> Option<i64> {
Duration::try_from_secs_f64(seconds)
.ok()
.and_then(|duration| i64::try_from(duration.as_millis()).ok())
}
#[derive(Debug)]
pub struct LongestWindow(AtomicU64);
impl Default for LongestWindow {
fn default() -> Self {
Self::new()
}
}
impl LongestWindow {
#[must_use]
pub const fn new() -> Self {
Self(AtomicU64::new(60.0_f64.to_bits()))
}
pub fn observe(&self, window: f64) {
if window > 0.0 {
_ = self.0.fetch_max(window.to_bits(), Ordering::Relaxed);
}
}
#[must_use]
pub fn prune_cutoff(&self, now: i64) -> Option<i64> {
millis(f64::from_bits(self.0.load(Ordering::Relaxed)))
.and_then(|milliseconds| now.checked_sub(milliseconds))
}
}