mod account;
pub use self::account::{Account, TokenBucket, charged, refund};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::time::Instant;
pub const METER_WINDOW_SECS: u64 = 10;
pub struct RequestLimits {
interval: Duration,
state: Mutex<Slots>,
gates: Mutex<HashMap<u32, Arc<Semaphore>>>,
networks: Option<Networks>,
}
struct Networks {
default: Duration,
rates: HashMap<u16, Duration>,
next: Mutex<HashMap<u16, Instant>>,
}
impl Networks {
fn reserve(&self, network: u16, now: Instant) -> Option<Instant> {
let interval = self.rates.get(&network).copied().unwrap_or(self.default);
if interval.is_zero() {
return None;
}
let mut next = self.next.lock().unwrap_or_else(PoisonError::into_inner);
let at = next.entry(network).or_insert(now);
let slot = (*at).max(now);
*at = slot + interval;
Some(slot)
}
}
fn spacing(per_second: u32) -> Duration {
match per_second {
0 => Duration::ZERO,
n => Duration::from_secs(1) / n,
}
}
#[must_use = "the device's gate is held only while the turn lives"]
pub struct Turn {
_permit: Option<OwnedSemaphorePermit>,
}
#[derive(Default)]
struct Slots {
next: Option<Instant>,
recent: VecDeque<Instant>,
}
const METER_WINDOW: Duration = Duration::from_secs(METER_WINDOW_SECS);
impl RequestLimits {
pub fn new(per_second: u32) -> Self {
Self {
interval: spacing(per_second),
state: Mutex::new(Slots::default()),
gates: Mutex::new(HashMap::new()),
networks: None,
}
}
pub fn with_network_rates(
mut self,
default: u32,
rates: impl IntoIterator<Item = (u16, u32)>,
) -> Self {
self.networks = Some(Networks {
default: spacing(default),
rates: rates
.into_iter()
.map(|(network, per_second)| (network, spacing(per_second)))
.collect(),
next: Mutex::new(HashMap::new()),
});
self
}
pub fn paces_networks(&self) -> bool {
self.networks.is_some()
}
pub fn gate(&self, device: u32) -> Arc<Semaphore> {
let mut gates = self.gates.lock().unwrap_or_else(PoisonError::into_inner);
Arc::clone(
gates
.entry(device)
.or_insert_with(|| Arc::new(Semaphore::new(1))),
)
}
pub async fn turn(&self, device: u32) -> Turn {
self.turn_on(device, None).await
}
pub async fn turn_on(&self, device: u32, network: Option<u16>) -> Turn {
let account = account::current();
let start = Instant::now();
if let Some(bucket) = account.as_deref().and_then(Account::bucket) {
bucket.take().await;
}
let permit = self.gate(device).acquire_owned().await.ok();
self.pace(network).await;
if let Some(account) = &account {
account.count_request();
account.add_wait(start.elapsed());
}
Turn { _permit: permit }
}
pub async fn slot(&self, network: Option<u16>) {
let account = account::current();
let start = Instant::now();
if let Some(bucket) = account.as_deref().and_then(Account::bucket) {
bucket.take().await;
}
self.pace(network).await;
if let Some(account) = &account {
account.count_request();
account.add_wait(start.elapsed());
}
}
async fn pace(&self, network: Option<u16>) {
if let (Some(networks), Some(network)) = (&self.networks, network)
&& let Some(slot) = networks.reserve(network, Instant::now())
{
tokio::time::sleep_until(slot).await;
}
self.wait_slot().await;
}
pub async fn wait_device_free(&self, device: u32) {
drop(self.gate(device).acquire_owned().await);
self.wait_free().await;
}
fn reserve(&self, now: Instant) -> Instant {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let slot = state.next.map_or(now, |next| next.max(now));
state.next = Some(slot + self.interval);
while state
.recent
.front()
.is_some_and(|at| now.saturating_duration_since(*at) >= METER_WINDOW)
{
state.recent.pop_front();
}
state.recent.push_back(slot);
slot
}
pub async fn acquire(&self) {
self.slot(None).await;
}
async fn wait_slot(&self) {
let slot = self.reserve(Instant::now());
if !self.interval.is_zero() {
tokio::time::sleep_until(slot).await;
}
}
pub async fn wait_free(&self) {
loop {
let next = self
.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.next;
match next {
Some(at) if at > Instant::now() => tokio::time::sleep_until(at).await,
_ => return,
}
}
}
pub fn recent(&self) -> usize {
let now = Instant::now();
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state
.recent
.iter()
.filter(|at| **at <= now && now.saturating_duration_since(**at) < METER_WINDOW)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(start_paused = true)]
async fn the_rate_cap_spaces_requests_evenly() {
let cap = RequestLimits::new(4);
let start = Instant::now();
let mut slots = Vec::new();
for _ in 0..3 {
cap.acquire().await;
slots.push(Instant::now() - start);
}
assert_eq!(
slots,
[
Duration::ZERO,
Duration::from_millis(250),
Duration::from_millis(500)
]
);
tokio::time::sleep(Duration::from_secs(1)).await;
let before = Instant::now();
cap.acquire().await;
assert_eq!(Instant::now(), before);
let none = RequestLimits::new(0);
let before = Instant::now();
for _ in 0..10 {
none.acquire().await;
}
assert_eq!(Instant::now(), before);
}
#[tokio::test(start_paused = true)]
async fn waiting_for_a_free_slot_takes_none() {
let cap = RequestLimits::new(4);
let start = Instant::now();
cap.acquire().await;
cap.wait_free().await;
assert_eq!(Instant::now() - start, Duration::from_millis(250));
cap.wait_free().await;
cap.acquire().await;
assert_eq!(Instant::now() - start, Duration::from_millis(250));
assert_eq!(cap.recent(), 2);
}
#[tokio::test(start_paused = true)]
async fn a_slow_network_is_held_to_its_rate() {
let limits = RequestLimits::new(0).with_network_rates(20, [(2001, 2)]);
assert!(limits.paces_networks());
let start = Instant::now();
let mut slow = Vec::new();
for device in 0..5 {
drop(limits.turn_on(device, Some(2001)).await);
slow.push((Instant::now() - start).as_millis());
}
assert_eq!(slow, [0, 500, 1000, 1500, 2000]);
let before = Instant::now();
for device in 10..14 {
drop(limits.turn_on(device, Some(0)).await);
}
assert_eq!(Instant::now() - before, Duration::from_millis(150));
let before = Instant::now();
drop(limits.turn_on(20, None).await);
assert_eq!(Instant::now(), before);
}
#[tokio::test(start_paused = true)]
async fn a_charged_call_counts_requests_and_waits() {
let limits = RequestLimits::new(10);
let bucket = Arc::new(TokenBucket::new(2, 1));
let account = Arc::new(Account::new(Some(bucket)));
charged(Arc::clone(&account), async {
for _ in 0..3 {
drop(limits.turn(1).await);
}
limits.slot(None).await;
})
.await;
assert_eq!(account.requests(), 4);
assert_eq!(account.throttled(), Duration::from_millis(1500));
drop(limits.turn(1).await);
assert_eq!(account.requests(), 4);
}
}