use std::fmt;
use std::str::FromStr;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use relay_base_schema::metrics::MetricNamespace;
use relay_base_schema::organization::OrganizationId;
use relay_base_schema::project::{ProjectId, ProjectKey};
use smallvec::SmallVec;
use crate::quota::{DataCategories, ItemScoping, Quota, QuotaScope, ReasonCode, Scoping};
use crate::REJECT_ALL_SECS;
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RetryAfter {
when: Instant,
}
impl RetryAfter {
#[inline]
pub fn from_secs(seconds: u64) -> Self {
let now = Instant::now();
let when = now.checked_add(Duration::from_secs(seconds)).unwrap_or(now);
Self { when }
}
#[inline]
pub fn remaining_at(self, at: Instant) -> Option<Duration> {
if at >= self.when {
None
} else {
Some(self.when - at)
}
}
#[inline]
pub fn remaining(self) -> Option<Duration> {
self.remaining_at(Instant::now())
}
#[inline]
pub fn remaining_seconds_at(self, at: Instant) -> u64 {
match self.remaining_at(at) {
Some(duration) if duration.subsec_nanos() == 0 => duration.as_secs(),
Some(duration) => duration.as_secs() + 1,
None => 0,
}
}
#[inline]
pub fn remaining_seconds(self) -> u64 {
self.remaining_seconds_at(Instant::now())
}
#[inline]
pub fn expired_at(self, at: Instant) -> bool {
self.remaining_at(at).is_none()
}
#[inline]
pub fn expired(self) -> bool {
self.remaining_at(Instant::now()).is_none()
}
}
impl fmt::Debug for RetryAfter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.remaining_seconds() {
0 => write!(f, "RetryAfter(expired)"),
remaining => write!(f, "RetryAfter({remaining}s)"),
}
}
}
#[cfg(test)]
impl serde::Serialize for RetryAfter {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeTupleStruct;
let mut tup = serializer.serialize_tuple_struct("RetryAfter", 1)?;
tup.serialize_field(&self.remaining_seconds())?;
tup.end()
}
}
#[derive(Debug)]
pub enum InvalidRetryAfter {
InvalidDelay(std::num::ParseFloatError),
}
impl FromStr for RetryAfter {
type Err = InvalidRetryAfter;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let float = s.parse::<f64>().map_err(InvalidRetryAfter::InvalidDelay)?;
let seconds = float.max(0.0).ceil() as u64;
Ok(RetryAfter::from_secs(seconds))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub enum RateLimitScope {
Global,
Organization(OrganizationId),
Project(ProjectId),
Key(ProjectKey),
}
impl RateLimitScope {
pub fn for_quota(scoping: &Scoping, scope: QuotaScope) -> Self {
match scope {
QuotaScope::Global => Self::Global,
QuotaScope::Organization => Self::Organization(scoping.organization_id),
QuotaScope::Project => Self::Project(scoping.project_id),
QuotaScope::Key => Self::Key(scoping.project_key),
QuotaScope::Unknown => Self::Key(scoping.project_key),
}
}
pub fn name(&self) -> &'static str {
match *self {
Self::Global => QuotaScope::Global.name(),
Self::Key(_) => QuotaScope::Key.name(),
Self::Project(_) => QuotaScope::Project.name(),
Self::Organization(_) => QuotaScope::Organization.name(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct RateLimit {
pub categories: DataCategories,
pub scope: RateLimitScope,
pub reason_code: Option<ReasonCode>,
pub retry_after: RetryAfter,
pub namespaces: SmallVec<[MetricNamespace; 1]>,
}
impl RateLimit {
pub fn from_quota(quota: &Quota, scoping: &Scoping, retry_after: RetryAfter) -> Self {
Self {
categories: quota.categories.clone(),
scope: RateLimitScope::for_quota(scoping, quota.scope),
reason_code: quota.reason_code.clone(),
retry_after,
namespaces: quota.namespace.into_iter().collect(),
}
}
pub fn matches(&self, scoping: ItemScoping<'_>) -> bool {
self.matches_scope(scoping)
&& scoping.matches_categories(&self.categories)
&& scoping.matches_namespaces(&self.namespaces)
}
fn matches_scope(&self, scoping: ItemScoping<'_>) -> bool {
match self.scope {
RateLimitScope::Global => true,
RateLimitScope::Organization(org_id) => scoping.organization_id == org_id,
RateLimitScope::Project(project_id) => scoping.project_id == project_id,
RateLimitScope::Key(ref key) => scoping.project_key == *key,
}
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct RateLimits {
limits: Vec<RateLimit>,
}
impl RateLimits {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, mut limit: RateLimit) {
limit.categories.sort();
let limit_opt = self.limits.iter_mut().find(|l| {
let RateLimit {
categories,
scope,
reason_code: _,
retry_after: _,
namespaces: namespace,
} = &limit;
*categories == l.categories && *scope == l.scope && *namespace == l.namespaces
});
match limit_opt {
None => self.limits.push(limit),
Some(existing) if existing.retry_after < limit.retry_after => *existing = limit,
Some(_) => (), }
}
pub fn merge(&mut self, limits: Self) {
for limit in limits {
self.add(limit);
}
}
pub fn is_ok(&self) -> bool {
!self.is_limited()
}
pub fn is_limited(&self) -> bool {
let now = Instant::now();
self.iter().any(|limit| !limit.retry_after.expired_at(now))
}
pub fn clean_expired(&mut self, now: Instant) {
self.limits
.retain(|limit| !limit.retry_after.expired_at(now));
}
pub fn check(&self, scoping: ItemScoping<'_>) -> Self {
self.check_with_quotas(&[], scoping)
}
pub fn check_with_quotas<'a>(
&self,
quotas: impl IntoIterator<Item = &'a Quota>,
scoping: ItemScoping<'_>,
) -> Self {
let mut applied_limits = Self::new();
for quota in quotas {
if quota.limit == Some(0) && quota.matches(scoping) {
let retry_after = RetryAfter::from_secs(REJECT_ALL_SECS);
applied_limits.add(RateLimit::from_quota(quota, &scoping, retry_after));
}
}
for limit in &self.limits {
if limit.matches(scoping) {
applied_limits.add(limit.clone());
}
}
applied_limits
}
pub fn iter(&self) -> RateLimitsIter<'_> {
RateLimitsIter {
iter: self.limits.iter(),
}
}
pub fn longest(&self) -> Option<&RateLimit> {
self.iter().max_by_key(|limit| limit.retry_after)
}
pub fn is_empty(&self) -> bool {
self.limits.is_empty()
}
}
pub struct RateLimitsIter<'a> {
iter: std::slice::Iter<'a, RateLimit>,
}
impl<'a> Iterator for RateLimitsIter<'a> {
type Item = &'a RateLimit;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}
impl IntoIterator for RateLimits {
type IntoIter = RateLimitsIntoIter;
type Item = RateLimit;
fn into_iter(self) -> Self::IntoIter {
RateLimitsIntoIter {
iter: self.limits.into_iter(),
}
}
}
pub struct RateLimitsIntoIter {
iter: std::vec::IntoIter<RateLimit>,
}
impl Iterator for RateLimitsIntoIter {
type Item = RateLimit;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}
impl<'a> IntoIterator for &'a RateLimits {
type IntoIter = RateLimitsIter<'a>;
type Item = &'a RateLimit;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Debug, Default)]
pub struct CachedRateLimits(Mutex<Arc<RateLimits>>);
impl CachedRateLimits {
pub fn new() -> Self {
Self::default()
}
pub fn add(&self, limit: RateLimit) {
let mut inner = self.0.lock().unwrap_or_else(PoisonError::into_inner);
let current = Arc::make_mut(&mut inner);
current.add(limit);
}
pub fn merge(&self, limits: RateLimits) {
let mut inner = self.0.lock().unwrap_or_else(PoisonError::into_inner);
let current = Arc::make_mut(&mut inner);
for limit in limits {
current.add(limit)
}
}
pub fn current_limits(&self) -> Arc<RateLimits> {
let now = Instant::now();
let mut inner = self.0.lock().unwrap_or_else(PoisonError::into_inner);
Arc::make_mut(&mut inner).clean_expired(now);
Arc::clone(&inner)
}
}
#[cfg(test)]
mod tests {
use smallvec::smallvec;
use super::*;
use crate::quota::DataCategory;
use crate::MetricNamespaceScoping;
#[test]
fn test_parse_retry_after() {
let retry_after = "17.1".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 18);
assert!(!retry_after.expired());
let retry_after = "17.7".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 18);
assert!(!retry_after.expired());
let retry_after = "17".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 17);
assert!(!retry_after.expired());
let retry_after = "-2".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 0);
assert!(retry_after.expired());
let retry_after = "-inf".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 0);
assert!(retry_after.expired());
let retry_after = "inf".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 0);
assert!(retry_after.expired());
let retry_after = "NaN".parse::<RetryAfter>().expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 0);
assert!(retry_after.expired());
let retry_after = "100000000000000000000"
.parse::<RetryAfter>()
.expect("parse RetryAfter");
assert_eq!(retry_after.remaining_seconds(), 0);
assert!(retry_after.expired());
"".parse::<RetryAfter>().expect_err("error RetryAfter");
"nope".parse::<RetryAfter>().expect_err("error RetryAfter");
" 2 ".parse::<RetryAfter>().expect_err("error RetryAfter");
"6 0".parse::<RetryAfter>().expect_err("error RetryAfter");
}
#[test]
fn test_rate_limit_matches_categories() {
let rate_limit = RateLimit {
categories: smallvec![DataCategory::Unknown, DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
};
assert!(rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
assert!(!rate_limit.matches(ItemScoping {
category: DataCategory::Transaction,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
}
#[test]
fn test_rate_limit_matches_organization() {
let rate_limit = RateLimit {
categories: DataCategories::new(),
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
};
assert!(rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
assert!(!rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(0),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
}
#[test]
fn test_rate_limit_matches_project() {
let rate_limit = RateLimit {
categories: DataCategories::new(),
scope: RateLimitScope::Project(ProjectId::new(21)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
};
assert!(rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
assert!(!rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(0),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
}
#[test]
fn test_rate_limit_matches_namespaces() {
let rate_limit = RateLimit {
categories: smallvec![],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![MetricNamespace::Custom],
};
let scoping = Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
};
assert!(rate_limit.matches(ItemScoping {
category: DataCategory::MetricBucket,
scoping: &scoping,
namespace: MetricNamespaceScoping::Some(MetricNamespace::Custom),
}));
assert!(!rate_limit.matches(ItemScoping {
category: DataCategory::MetricBucket,
scoping: &scoping,
namespace: MetricNamespaceScoping::Some(MetricNamespace::Spans),
}));
let general_rate_limit = RateLimit {
categories: smallvec![],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![], };
assert!(general_rate_limit.matches(ItemScoping {
category: DataCategory::MetricBucket,
scoping: &scoping,
namespace: MetricNamespaceScoping::Some(MetricNamespace::Spans),
}));
assert!(general_rate_limit.matches(ItemScoping {
category: DataCategory::MetricBucket,
scoping: &scoping,
namespace: MetricNamespaceScoping::None,
}));
}
#[test]
fn test_rate_limit_matches_key() {
let rate_limit = RateLimit {
categories: DataCategories::new(),
scope: RateLimitScope::Key(
ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
};
assert!(rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
assert!(!rate_limit.matches(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(0),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("deadbeefdeadbeefdeadbeefdeadbeef").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
}));
}
#[test]
fn test_rate_limits_add_replacement() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Default, DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("first")),
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error, DataCategory::Default],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("second")),
retry_after: RetryAfter::from_secs(10),
namespaces: smallvec![],
});
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
default,
error,
],
scope: Organization(OrganizationId(42)),
reason_code: Some(ReasonCode("second")),
retry_after: RetryAfter(10),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_add_shadowing() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Default, DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("first")),
retry_after: RetryAfter::from_secs(10),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error, DataCategory::Default],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("second")),
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
default,
error,
],
scope: Organization(OrganizationId(42)),
reason_code: Some(ReasonCode("first")),
retry_after: RetryAfter(10),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_add_buckets() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Transaction],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Project(ProjectId::new(21)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
RateLimit(
categories: [
transaction,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
RateLimit(
categories: [
error,
],
scope: Project(ProjectId(21)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_add_namespaces() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::MetricBucket],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![MetricNamespace::Custom],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::MetricBucket],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![MetricNamespace::Spans],
});
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
metric_bucket,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [
"custom",
],
),
RateLimit(
categories: [
metric_bucket,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [
"spans",
],
),
],
)
"###);
}
#[test]
fn test_rate_limits_longest() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("first")),
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Transaction],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("second")),
retry_after: RetryAfter::from_secs(10),
namespaces: smallvec![],
});
let rate_limit = rate_limits.longest().unwrap();
insta::assert_ron_snapshot!(rate_limit, @r###"
RateLimit(
categories: [
transaction,
],
scope: Organization(OrganizationId(42)),
reason_code: Some(ReasonCode("second")),
retry_after: RetryAfter(10),
namespaces: [],
)
"###);
}
#[test]
fn test_rate_limits_clean_expired() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Project(ProjectId::new(21)),
reason_code: None,
retry_after: RetryAfter::from_secs(0),
namespaces: smallvec![],
});
assert_eq!(rate_limits.iter().count(), 2);
rate_limits.clean_expired(Instant::now());
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_check() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Transaction],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
let applied_limits = rate_limits.check(ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
});
insta::assert_ron_snapshot!(applied_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_check_quotas() {
let mut rate_limits = RateLimits::new();
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits.add(RateLimit {
categories: smallvec![DataCategory::Transaction],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
let item_scoping = ItemScoping {
category: DataCategory::Error,
scoping: &Scoping {
organization_id: OrganizationId::new(42),
project_id: ProjectId::new(21),
project_key: ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(),
key_id: None,
},
namespace: MetricNamespaceScoping::None,
};
let quotas = &[Quota {
id: None,
categories: smallvec![DataCategory::Error],
scope: QuotaScope::Organization,
scope_id: Some("42".to_owned()),
limit: Some(0),
window: None,
reason_code: Some(ReasonCode::new("zero")),
namespace: None,
}];
let applied_limits = rate_limits.check_with_quotas(quotas, item_scoping);
insta::assert_ron_snapshot!(applied_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: Some(ReasonCode("zero")),
retry_after: RetryAfter(60),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_rate_limits_merge() {
let mut rate_limits1 = RateLimits::new();
let mut rate_limits2 = RateLimits::new();
rate_limits1.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("first")),
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits1.add(RateLimit {
categories: smallvec![DataCategory::TransactionIndexed],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
rate_limits2.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: Some(ReasonCode::new("second")),
retry_after: RetryAfter::from_secs(10),
namespaces: smallvec![],
});
rate_limits1.merge(rate_limits2);
insta::assert_ron_snapshot!(rate_limits1, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: Some(ReasonCode("second")),
retry_after: RetryAfter(10),
namespaces: [],
),
RateLimit(
categories: [
transaction_indexed,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
],
)
"###);
}
#[test]
fn test_cached_rate_limits_expired() {
let cached = CachedRateLimits::new();
cached.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Organization(OrganizationId::new(42)),
reason_code: None,
retry_after: RetryAfter::from_secs(1),
namespaces: smallvec![],
});
cached.add(RateLimit {
categories: smallvec![DataCategory::Error],
scope: RateLimitScope::Project(ProjectId::new(21)),
reason_code: None,
retry_after: RetryAfter::from_secs(0),
namespaces: smallvec![],
});
let rate_limits = cached.current_limits();
insta::assert_ron_snapshot!(rate_limits, @r###"
RateLimits(
limits: [
RateLimit(
categories: [
error,
],
scope: Organization(OrganizationId(42)),
reason_code: None,
retry_after: RetryAfter(1),
namespaces: [],
),
],
)
"###);
}
}