use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use crate::engine::CancelToken;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Rate {
pub count: u64,
pub per_seconds: u64,
#[serde(default = "one")]
pub burst: u64,
}
fn one() -> u64 {
1
}
impl Rate {
pub fn parse(s: &str) -> Result<Rate, String> {
let bad = || {
format!(
"`{s}` isn't a rate; write a count and a unit, like `100/minute`, `5/second` or `20000/hour`"
)
};
let (n, unit) = s.split_once('/').ok_or_else(bad)?;
let count: u64 = n.trim().replace('_', "").parse().map_err(|_| bad())?;
if count == 0 {
return Err(format!("`{s}`: the count must be 1 or more"));
}
let per_seconds = match unit.trim().to_ascii_lowercase().trim_end_matches('s') {
"second" | "sec" | "" => 1,
"minute" | "min" | "m" => 60,
"hour" | "hr" | "h" => 3600,
_ => return Err(bad()),
};
Ok(Rate {
count,
per_seconds,
burst: 1,
})
}
pub fn interval(&self) -> Duration {
Duration::from_secs_f64(self.per_seconds as f64 / self.count as f64)
}
pub fn shown(&self) -> String {
let unit = match self.per_seconds {
1 => "second",
60 => "minute",
_ => "hour",
};
format!("{}/{unit}", self.count)
}
}
#[derive(Default)]
pub struct Pacer {
next: Mutex<HashMap<String, Instant>>,
}
impl Pacer {
pub fn wait(&self, key: &str, rate: &Rate, cancel: &CancelToken) -> bool {
let gap = rate.interval();
let slack = gap * (rate.burst.max(1) - 1) as u32;
let until = {
let mut next = self.next.lock().unwrap_or_else(|e| e.into_inner());
let now = Instant::now();
let tat = next.get(key).copied().unwrap_or(now).max(now);
let allowed = tat.checked_sub(slack).map_or(now, |t| t.max(now));
next.insert(key.to_string(), tat + gap);
allowed
};
while Instant::now() < until {
if cancel.is_cancelled() {
return false;
}
std::thread::sleep((until - Instant::now()).min(Duration::from_millis(100)));
}
!cancel.is_cancelled()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_rates() {
assert_eq!(
Rate::parse("100/minute").unwrap().interval(),
Duration::from_millis(600)
);
assert_eq!(
Rate::parse("5/second").unwrap().interval(),
Duration::from_millis(200)
);
assert_eq!(
Rate::parse("3600/hour").unwrap().interval(),
Duration::from_secs(1)
);
assert_eq!(Rate::parse("60 / Minutes").unwrap().shown(), "60/minute");
assert_eq!(Rate::parse("20_000/h").unwrap().count, 20_000);
for bad in ["100", "0/minute", "x/minute", "10/day"] {
assert!(Rate::parse(bad).is_err(), "{bad}");
}
}
#[test]
fn spaces_evenly_and_allows_a_burst() {
let cancel = CancelToken::new();
let p = Pacer::default();
let even = Rate::parse("20/second").unwrap();
let t = Instant::now();
for _ in 0..5 {
assert!(p.wait("a", &even, &cancel));
}
assert!(t.elapsed() >= Duration::from_millis(190), "{:?}", t.elapsed());
let burst = Rate {
burst: 5,
..Rate::parse("2/second").unwrap()
};
let t = Instant::now();
for _ in 0..5 {
assert!(p.wait("b", &burst, &cancel));
}
assert!(t.elapsed() < Duration::from_millis(100), "{:?}", t.elapsed());
assert!(p.wait("b", &burst, &cancel));
assert!(t.elapsed() >= Duration::from_millis(400), "{:?}", t.elapsed());
}
}