use std::future::Future;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::time::Instant;
tokio::task_local! {
static ACCOUNT: Arc<Account>;
}
#[derive(Debug, Default)]
pub struct Account {
requests: AtomicU32,
throttled_us: AtomicU64,
bucket: Option<Arc<TokenBucket>>,
}
impl Account {
pub fn new(bucket: Option<Arc<TokenBucket>>) -> Self {
Self {
requests: AtomicU32::new(0),
throttled_us: AtomicU64::new(0),
bucket,
}
}
pub fn requests(&self) -> u32 {
self.requests.load(Ordering::Relaxed)
}
pub fn throttled(&self) -> Duration {
Duration::from_micros(self.throttled_us.load(Ordering::Relaxed))
}
pub(super) fn count_request(&self) {
self.requests.fetch_add(1, Ordering::Relaxed);
}
fn uncount_request(&self) {
if self.requests.load(Ordering::Relaxed) > 0 {
self.requests.fetch_sub(1, Ordering::Relaxed);
}
}
pub(super) fn add_wait(&self, waited: Duration) {
let micros = u64::try_from(waited.as_micros()).unwrap_or(u64::MAX);
self.throttled_us.fetch_add(micros, Ordering::Relaxed);
}
pub(super) fn bucket(&self) -> Option<&TokenBucket> {
self.bucket.as_deref()
}
}
pub async fn charged<F: Future>(account: Arc<Account>, future: F) -> F::Output {
ACCOUNT.scope(account, future).await
}
pub fn refund() {
if let Some(account) = current() {
account.uncount_request();
}
}
pub(super) fn current() -> Option<Arc<Account>> {
ACCOUNT.try_with(Arc::clone).ok()
}
#[derive(Debug)]
pub struct TokenBucket {
per_second: f64,
burst: f64,
state: Mutex<Tokens>,
}
#[derive(Debug)]
struct Tokens {
left: f64,
at: Instant,
}
impl TokenBucket {
pub fn new(per_second: u32, burst: u32) -> Self {
let burst = f64::from(burst.max(1));
Self {
per_second: f64::from(per_second),
burst,
state: Mutex::new(Tokens {
left: burst,
at: Instant::now(),
}),
}
}
pub async fn take(&self) {
if self.per_second == 0.0 {
return;
}
let wait = {
let mut tokens = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let now = Instant::now();
let refill = now.saturating_duration_since(tokens.at).as_secs_f64() * self.per_second;
tokens.left = (tokens.left + refill).min(self.burst) - 1.0;
tokens.at = now;
(tokens.left < 0.0).then(|| Duration::from_secs_f64(-tokens.left / self.per_second))
};
if let Some(wait) = wait {
tokio::time::sleep(wait).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(start_paused = true)]
async fn a_bucket_gives_its_burst_then_its_rate() {
let bucket = TokenBucket::new(5, 3);
let start = Instant::now();
let mut times = Vec::new();
for _ in 0..5 {
bucket.take().await;
times.push((Instant::now() - start).as_millis());
}
assert_eq!(times, [0, 0, 0, 200, 400]);
tokio::time::sleep(Duration::from_secs(10)).await;
let before = Instant::now();
for _ in 0..3 {
bucket.take().await;
}
assert_eq!(Instant::now(), before, "refilled to the burst, no more");
bucket.take().await;
assert_eq!(Instant::now() - before, Duration::from_millis(200));
let unlimited = TokenBucket::new(0, 1);
let before = Instant::now();
for _ in 0..100 {
unlimited.take().await;
}
assert_eq!(Instant::now(), before);
}
#[tokio::test]
async fn the_account_rides_with_the_call() {
assert!(current().is_none());
let account = Arc::new(Account::new(None));
charged(Arc::clone(&account), async {
current().expect("an account").count_request();
})
.await;
assert_eq!(account.requests(), 1);
}
}