use std::{
sync::atomic::AtomicU64,
time::{Duration, Instant},
};
use crate::TrypemaError;
pub(crate) const MILLISECONDS_PER_SECOND: u64 = 1_000;
pub(crate) const SECONDS_PER_MINUTE: u64 = 60;
const MINUTES_PER_HOUR: u64 = 60;
const HOURS_PER_DAY: u64 = 24;
const DAYS_PER_WEEK: u64 = 7;
const DAYS_PER_MONTH: u64 = 30;
pub(crate) const SECONDS_PER_HOUR: u64 = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
const SECONDS_PER_DAY: u64 = SECONDS_PER_HOUR * HOURS_PER_DAY;
const SECONDS_PER_WEEK: u64 = SECONDS_PER_DAY * DAYS_PER_WEEK;
const SECONDS_PER_MONTH: u64 = SECONDS_PER_DAY * DAYS_PER_MONTH;
pub(crate) fn checked_duration(
value: u64,
multiplier: u64,
name: &str,
error: fn(String) -> TrypemaError,
) -> Result<u64, TrypemaError> {
if value == 0 {
return Err(error(format!("{name} must be greater than 0")));
}
value
.checked_mul(multiplier)
.ok_or_else(|| error(format!("{name} is too large")))
}
pub(crate) fn duration_from_milliseconds(milliseconds: u128) -> Duration {
let seconds = milliseconds / u128::from(MILLISECONDS_PER_SECOND);
if seconds > u128::from(u64::MAX) {
return Duration::MAX;
}
let subsec_milliseconds = milliseconds % u128::from(MILLISECONDS_PER_SECOND);
Duration::new(seconds as u64, (subsec_milliseconds as u32) * 1_000_000)
}
pub(crate) type RandomState = ahash::RandomState;
#[derive(Debug)]
pub(crate) struct Bucket {
pub count: AtomicU64,
pub declined_count: AtomicU64,
pub timestamp: Instant,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum_macros::Display)]
pub enum RateLimitDecision {
Allowed,
#[non_exhaustive]
Rejected {
window_size: WindowSize,
retry_after: Duration,
remaining_after_waiting: u64,
},
#[non_exhaustive]
Suppressed {
suppression_factor: f64,
is_allowed: bool,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct SuppressedRateLimitSnapshot {
pub total: u64,
pub total_declined: u64,
pub suppression_factor: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConditionalSetOutcome {
pub matched: bool,
pub previous_total: u64,
pub current_total: u64,
}
#[cfg(test)]
impl PartialEq<(u64, u64)> for ConditionalSetOutcome {
fn eq(&self, other: &(u64, u64)) -> bool {
(self.current_total, self.previous_total) == *other
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RateLimit(f64);
impl RateLimit {
pub fn max() -> Self {
Self(f64::MAX)
}
pub fn as_per_second(self) -> f64 {
self.0
}
pub fn as_per_minute(self) -> f64 {
self.as_per_period(SECONDS_PER_MINUTE)
}
pub fn as_per_hour(self) -> f64 {
self.as_per_period(SECONDS_PER_HOUR)
}
pub fn as_per_day(self) -> f64 {
self.as_per_period(SECONDS_PER_DAY)
}
pub fn as_per_week(self) -> f64 {
self.as_per_period(SECONDS_PER_WEEK)
}
pub fn as_per_month(self) -> f64 {
self.as_per_period(SECONDS_PER_MONTH)
}
pub fn per_second(value: f64) -> Result<Self, TrypemaError> {
Self::from_per_second(value)
}
pub fn per_second_or_panic(value: f64) -> Self {
Self::per_second(value).unwrap()
}
pub fn per_minute(value: f64) -> Result<Self, TrypemaError> {
Self::from_period(value, SECONDS_PER_MINUTE)
}
pub fn per_minute_or_panic(value: f64) -> Self {
Self::per_minute(value).unwrap()
}
pub fn per_hour(value: f64) -> Result<Self, TrypemaError> {
Self::from_period(value, SECONDS_PER_HOUR)
}
pub fn per_hour_or_panic(value: f64) -> Self {
Self::per_hour(value).unwrap()
}
pub fn per_day(value: f64) -> Result<Self, TrypemaError> {
Self::from_period(value, SECONDS_PER_DAY)
}
pub fn per_day_or_panic(value: f64) -> Self {
Self::per_day(value).unwrap()
}
pub fn per_week(value: f64) -> Result<Self, TrypemaError> {
Self::from_period(value, SECONDS_PER_WEEK)
}
pub fn per_week_or_panic(value: f64) -> Self {
Self::per_week(value).unwrap()
}
pub fn per_month(value: f64) -> Result<Self, TrypemaError> {
Self::from_period(value, SECONDS_PER_MONTH)
}
pub fn per_month_or_panic(value: f64) -> Self {
Self::per_month(value).unwrap()
}
fn from_period(value: f64, period_seconds: u64) -> Result<Self, TrypemaError> {
Self::from_per_second(value / (period_seconds as f64))
}
fn as_per_period(self, period_seconds: u64) -> f64 {
self.0 * (period_seconds as f64)
}
fn from_per_second(value: f64) -> Result<Self, TrypemaError> {
if !value.is_finite() || value <= 0f64 {
Err(TrypemaError::InvalidRateLimit(
"rate limit must be greater than 0".to_string(),
))
} else {
Ok(Self(value))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct WindowSize(u64);
impl Default for WindowSize {
fn default() -> Self {
Self(10)
}
}
impl WindowSize {
pub fn as_seconds(self) -> u64 {
self.0
}
pub fn as_milliseconds(self) -> u128 {
u128::from(self.0) * u128::from(MILLISECONDS_PER_SECOND)
}
pub fn as_minutes(self) -> f64 {
self.as_period(SECONDS_PER_MINUTE)
}
pub fn as_hours(self) -> f64 {
self.as_period(SECONDS_PER_HOUR)
}
pub fn as_days(self) -> f64 {
self.as_period(SECONDS_PER_DAY)
}
pub fn as_weeks(self) -> f64 {
self.as_period(SECONDS_PER_WEEK)
}
pub fn as_months(self) -> f64 {
self.as_period(SECONDS_PER_MONTH)
}
pub fn seconds(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, 1)
}
pub fn seconds_or_panic(value: u64) -> Self {
Self::seconds(value).unwrap()
}
pub fn minutes(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, SECONDS_PER_MINUTE)
}
pub fn minutes_or_panic(value: u64) -> Self {
Self::minutes(value).unwrap()
}
pub fn hours(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, SECONDS_PER_HOUR)
}
pub fn hours_or_panic(value: u64) -> Self {
Self::hours(value).unwrap()
}
pub fn days(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, SECONDS_PER_DAY)
}
pub fn days_or_panic(value: u64) -> Self {
Self::days(value).unwrap()
}
pub fn weeks(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, SECONDS_PER_WEEK)
}
pub fn weeks_or_panic(value: u64) -> Self {
Self::weeks(value).unwrap()
}
pub fn months(value: u64) -> Result<Self, TrypemaError> {
Self::from_seconds(value, SECONDS_PER_MONTH)
}
pub fn months_or_panic(value: u64) -> Self {
Self::months(value).unwrap()
}
fn from_seconds(value: u64, multiplier: u64) -> Result<Self, TrypemaError> {
checked_duration(
value,
multiplier,
"window size",
TrypemaError::InvalidWindowSize,
)
.map(Self)
}
fn as_period(self, period_seconds: u64) -> f64 {
(self.0 as f64) / (period_seconds as f64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct BucketSize(u64);
impl Default for BucketSize {
fn default() -> Self {
Self(100)
}
}
impl BucketSize {
pub fn as_milliseconds(self) -> u64 {
self.0
}
pub fn milliseconds(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, 1)
}
pub fn milliseconds_or_panic(value: u64) -> Self {
Self::milliseconds(value).unwrap()
}
pub fn seconds(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND)
}
pub fn seconds_or_panic(value: u64) -> Self {
Self::seconds(value).unwrap()
}
pub fn minutes(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE)
}
pub fn minutes_or_panic(value: u64) -> Self {
Self::minutes(value).unwrap()
}
pub fn hours(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_HOUR)
}
pub fn hours_or_panic(value: u64) -> Self {
Self::hours(value).unwrap()
}
pub fn days(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_DAY)
}
pub fn days_or_panic(value: u64) -> Self {
Self::days(value).unwrap()
}
pub fn weeks(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_WEEK)
}
pub fn weeks_or_panic(value: u64) -> Self {
Self::weeks(value).unwrap()
}
pub fn months(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_MONTH)
}
pub fn months_or_panic(value: u64) -> Self {
Self::months(value).unwrap()
}
fn from_milliseconds(value: u64, multiplier: u64) -> Result<Self, TrypemaError> {
checked_duration(
value,
multiplier,
"bucket size",
TrypemaError::InvalidBucketSize,
)
.map(Self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct HardLimitFactor(f64);
impl Default for HardLimitFactor {
fn default() -> Self {
Self(1f64)
}
}
impl HardLimitFactor {
pub fn as_multiplier(self) -> f64 {
self.0
}
pub fn new(value: f64) -> Result<Self, TrypemaError> {
Self::try_from(value)
}
pub fn new_or_panic(value: f64) -> Self {
Self::try_from(value).unwrap()
}
}
impl TryFrom<f64> for HardLimitFactor {
type Error = TrypemaError;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if !value.is_finite() || value < 1f64 {
Err(TrypemaError::InvalidHardLimitFactor(
"Hard limit factor must be greater than or equal to 1".to_string(),
))
} else {
Ok(Self(value))
}
}
}
#[cfg(any(feature = "redis-tokio", feature = "redis-smol"))]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum_macros::Display)]
pub(crate) enum RateType {
#[strum(to_string = "absolute")]
Absolute,
#[strum(to_string = "suppressed")]
Suppressed,
#[strum(to_string = "hybrid_absolute")]
HybridAbsolute,
#[strum(to_string = "hybrid_suppressed")]
HybridSuppressed,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct SuppressionFactorCachePeriod(u64);
impl Default for SuppressionFactorCachePeriod {
fn default() -> Self {
Self(100)
}
}
impl SuppressionFactorCachePeriod {
pub fn as_milliseconds(self) -> u64 {
self.0
}
pub fn milliseconds(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, 1)
}
pub fn milliseconds_or_panic(value: u64) -> Self {
Self::milliseconds(value).unwrap()
}
pub fn seconds(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND)
}
pub fn seconds_or_panic(value: u64) -> Self {
Self::seconds(value).unwrap()
}
pub fn minutes(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE)
}
pub fn minutes_or_panic(value: u64) -> Self {
Self::minutes(value).unwrap()
}
pub fn hours(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_HOUR)
}
pub fn hours_or_panic(value: u64) -> Self {
Self::hours(value).unwrap()
}
pub fn days(value: u64) -> Result<Self, TrypemaError> {
Self::from_milliseconds(value, MILLISECONDS_PER_SECOND * SECONDS_PER_DAY)
}
pub fn days_or_panic(value: u64) -> Self {
Self::days(value).unwrap()
}
fn from_milliseconds(value: u64, multiplier: u64) -> Result<Self, TrypemaError> {
checked_duration(
value,
multiplier,
"suppression-factor cache period",
TrypemaError::InvalidSuppressionFactorCachePeriod,
)
.map(Self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitComparator {
Eq(u64),
Lt(u64),
Gt(u64),
Ne(u64),
Always,
}
impl RateLimitComparator {
pub fn matches(self, current: u64) -> bool {
match self {
Self::Eq(operand) => current == operand,
Self::Lt(operand) => current < operand,
Self::Gt(operand) => current > operand,
Self::Ne(operand) => current != operand,
Self::Always => true,
}
}
#[cfg(any(feature = "redis-tokio", feature = "redis-smol"))]
pub(crate) fn redis_args(self) -> (&'static str, u64) {
match self {
Self::Eq(operand) => ("eq", operand),
Self::Lt(operand) => ("lt", operand),
Self::Gt(operand) => ("gt", operand),
Self::Ne(operand) => ("ne", operand),
Self::Always => ("nil", 0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HistoryPreservation {
PreserveNewest,
PreserveOldest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HistoryUpdateMode {
Replace,
Preserve(HistoryPreservation),
}
impl HistoryUpdateMode {
#[cfg(any(feature = "redis-tokio", feature = "redis-smol"))]
pub(crate) fn redis_arg(self) -> &'static str {
match self {
Self::Replace => "replace",
Self::Preserve(HistoryPreservation::PreserveNewest) => "preserve_newest",
Self::Preserve(HistoryPreservation::PreserveOldest) => "preserve_oldest",
}
}
}