use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::{BuildHasher, Hash};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::RwLock;
use crate::metrics::{AtomicRateMetrics, RateMetrics};
use crate::outcome::RateLimitResult;
use crate::quota::Quota;
use crate::state::AtomicGcraState;
#[repr(align(64))]
struct CachePadded<T> {
value: T,
}
impl<T> CachePadded<T> {
pub const fn new(value: T) -> Self {
Self { value }
}
}
impl<T> Deref for CachePadded<T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for CachePadded<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
struct KeyedShard<K, S> {
states: RwLock<HashMap<K, AtomicGcraState, S>>,
}
struct KeyedLimiterShared<K, S> {
shards: Box<[CachePadded<KeyedShard<K, S>>]>,
shard_mask: usize,
quota: Quota,
epoch: Instant,
hasher: S,
metrics: AtomicRateMetrics,
}
pub struct KeyedRateLimiter<K, S = ahash::RandomState> {
inner: Arc<KeyedLimiterShared<K, S>>,
}
impl<K, S> Clone for KeyedRateLimiter<K, S> {
#[inline]
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<K> KeyedRateLimiter<K, ahash::RandomState>
where
K: Eq + Hash,
{
pub fn new(quota: Quota) -> Self {
Self::with_shards(quota, 64)
}
pub fn with_shards(quota: Quota, num_shards: usize) -> Self {
Self::with_shards_and_hasher(quota, num_shards, ahash::RandomState::new())
}
pub fn per_second(requests: u32) -> Self {
Self::new(Quota::per_second(requests))
}
pub fn per_minute(requests: u32) -> Self {
Self::new(Quota::per_minute(requests))
}
}
impl<K, S> KeyedRateLimiter<K, S>
where
K: Eq + Hash,
S: BuildHasher + Clone,
{
pub fn with_shards_and_hasher(quota: Quota, num_shards: usize, hasher: S) -> Self {
let shards_count = num_shards.max(1).next_power_of_two();
let mut shards = Vec::with_capacity(shards_count);
for _ in 0..shards_count {
shards.push(CachePadded::new(KeyedShard {
states: RwLock::new(HashMap::with_hasher(hasher.clone())),
}));
}
Self {
inner: Arc::new(KeyedLimiterShared {
shards: shards.into_boxed_slice(),
shard_mask: shards_count - 1,
quota,
epoch: Instant::now(),
hasher,
metrics: AtomicRateMetrics::new(),
}),
}
}
#[inline]
fn now_nanos(&self) -> u64 {
self.inner.epoch.elapsed().as_nanos() as u64
}
#[inline]
fn select_shard<Q>(&self, key: &Q) -> &KeyedShard<K, S>
where
Q: Hash + ?Sized,
{
let hash = self.inner.hasher.hash_one(key);
let index = (hash as usize) & self.inner.shard_mask;
&self.inner.shards[index]
}
pub fn check<Q>(&self, key: &Q) -> RateLimitResult
where
K: Borrow<Q>,
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
{
self.check_n(key, 1)
}
pub fn check_n<Q>(&self, key: &Q, count: u32) -> RateLimitResult
where
K: Borrow<Q>,
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
{
let shard = self.select_shard(key);
let now = self.now_nanos();
{
let guard = shard.states.read();
if let Some(state) = guard.get(key) {
let outcome = state.check(now, &self.inner.quota, count);
if outcome.is_allowed() {
self.inner.metrics.record_allowed();
} else {
self.inner.metrics.record_limited();
}
return outcome;
}
}
let mut guard = shard.states.write();
let state = guard
.entry(key.to_owned())
.or_insert_with(|| AtomicGcraState::new(now));
let outcome = state.check(now, &self.inner.quota, count);
if outcome.is_allowed() {
self.inner.metrics.record_allowed();
} else {
self.inner.metrics.record_limited();
}
outcome
}
pub fn clean_idle(&self, max_idle: Duration) -> usize {
let now = self.now_nanos();
let max_idle_nanos = max_idle.as_nanos() as u64;
let mut total_purged = 0;
for shard in self.inner.shards.iter() {
let mut guard = shard.states.write();
let initial_len = guard.len();
guard.retain(|_, state| !state.is_idle(now, max_idle_nanos));
total_purged += initial_len - guard.len();
}
total_purged
}
pub fn len(&self) -> usize {
self.inner
.shards
.iter()
.map(|s| s.states.read().len())
.sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
for shard in self.inner.shards.iter() {
shard.states.write().clear();
}
}
#[inline]
pub fn quota(&self) -> &Quota {
&self.inner.quota
}
#[inline]
pub fn num_shards(&self) -> usize {
self.inner.shards.len()
}
#[inline]
pub fn metrics(&self) -> RateMetrics {
self.inner.metrics.snapshot()
}
#[inline]
pub fn reset_metrics(&self) {
self.inner.metrics.reset();
}
}