relay_cardinality/redis/
quota.rs1use hash32::Hasher;
2use std::fmt::{self, Write};
3use std::hash::Hash;
4
5use relay_base_schema::metrics::{MetricName, MetricNamespace, MetricType};
6use relay_base_schema::organization::OrganizationId;
7use relay_base_schema::project::ProjectId;
8use relay_common::time::UnixTimestamp;
9
10use crate::limiter::Entry;
11use crate::redis::{KEY_PREFIX, KEY_VERSION};
12use crate::window::Slot;
13use crate::{CardinalityLimit, CardinalityScope, Scoping, SlidingWindow};
14
15#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
21pub struct PartialQuotaScoping {
22 pub organization_id: Option<OrganizationId>,
23 pub project_id: Option<ProjectId>,
24 pub namespace: Option<MetricNamespace>,
25 window: SlidingWindow,
26 scope: CardinalityScope,
27}
28
29impl PartialQuotaScoping {
30 pub fn new(scoping: Scoping, limit: &CardinalityLimit) -> Option<Self> {
34 let (organization_id, project_id) = match limit.scope {
35 CardinalityScope::Organization => (Some(scoping.organization_id), None),
36 CardinalityScope::Project => (Some(scoping.organization_id), Some(scoping.project_id)),
37 CardinalityScope::Type => (Some(scoping.organization_id), Some(scoping.project_id)),
38 CardinalityScope::Name => (Some(scoping.organization_id), Some(scoping.project_id)),
39 CardinalityScope::Unknown => return None,
41 };
42
43 Some(Self {
44 organization_id,
45 project_id,
46 namespace: limit.namespace,
47 window: limit.window,
48 scope: limit.scope,
49 })
50 }
51
52 pub fn matches(&self, entry: &Entry) -> bool {
54 self.namespace.is_none() || self.namespace == Some(entry.namespace)
55 }
56
57 pub fn active_slot(&self, timestamp: UnixTimestamp) -> Slot {
59 self.window.active_slot(self.shifted(timestamp))
60 }
61
62 pub fn slots(&self, timestamp: UnixTimestamp) -> impl Iterator<Item = Slot> {
64 self.window.iter(self.shifted(timestamp))
65 }
66
67 fn shifted(&self, timestamp: UnixTimestamp) -> UnixTimestamp {
72 let shift = self
73 .organization_id
74 .map(|o| o.value() % self.window.granularity_seconds)
75 .unwrap_or(0);
76
77 UnixTimestamp::from_secs(timestamp.as_secs() + shift)
78 }
79
80 pub fn complete(self, entry: Entry<'_>) -> QuotaScoping {
86 let metric_name = match self.scope {
87 CardinalityScope::Name => Some(entry.name.clone()),
88 _ => None,
89 };
90 let metric_type = match self.scope {
91 CardinalityScope::Type => entry.name.try_type(),
92 _ => None,
93 };
94
95 QuotaScoping {
96 parent: self,
97 metric_type,
98 metric_name,
99 }
100 }
101}
102
103#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
109pub struct QuotaScoping {
110 parent: PartialQuotaScoping,
111 pub metric_type: Option<MetricType>,
112 pub metric_name: Option<MetricName>,
113}
114
115impl QuotaScoping {
116 pub fn redis_key_ttl(&self) -> u64 {
118 self.window.window_seconds
119 }
120
121 pub fn to_redis_key(&self, slot: Slot) -> String {
123 let organization_id = self.organization_id.unwrap_or(OrganizationId::new(0));
124 let project_id = self.project_id.map(|p| p.value()).unwrap_or(0);
125 let namespace = self.namespace.map(|ns| ns.as_str()).unwrap_or("");
126 let metric_type = DisplayOptMinus(self.metric_type);
127 let metric_name = DisplayOptMinus(self.metric_name.as_deref().map(fnv32));
128
129 let mut result = String::with_capacity(200);
133 write!(
134 &mut result,
135 "{KEY_PREFIX}:{KEY_VERSION}:scope-{{{organization_id}-{project_id}-{namespace}}}-{metric_type}{metric_name}{slot}"
136 )
137 .expect("formatting into a string never fails");
138
139 result
140 }
141}
142
143impl std::ops::Deref for QuotaScoping {
144 type Target = PartialQuotaScoping;
145
146 fn deref(&self) -> &Self::Target {
147 &self.parent
148 }
149}
150
151impl From<&QuotaScoping> for QuotaScoping {
153 fn from(value: &QuotaScoping) -> Self {
154 value.clone()
155 }
156}
157
158impl fmt::Debug for QuotaScoping {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 let PartialQuotaScoping {
161 organization_id,
162 project_id,
163 namespace,
164 window,
165 scope,
166 } = &self.parent;
167
168 f.debug_struct("QuotaScoping")
169 .field("organization_id", organization_id)
170 .field("project_id", project_id)
171 .field("namespace", namespace)
172 .field("window", window)
173 .field("scope", scope)
174 .field("metric_type", &self.metric_type)
175 .field("metric_name", &self.metric_name)
176 .finish()
177 }
178}
179
180struct DisplayOptMinus<T>(Option<T>);
181
182impl<T: fmt::Display> fmt::Display for DisplayOptMinus<T> {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 if let Some(inner) = self.0.as_ref() {
185 write!(f, "{inner}-")
186 } else {
187 Ok(())
188 }
189 }
190}
191
192fn fnv32(s: &str) -> u32 {
193 let mut hasher = hash32::FnvHasher::default();
194 s.hash(&mut hasher);
195 hasher.finish32()
196}