Skip to content

Commit 79fd7ca

Browse files
authored
feat(cardinality): Implement name based cardinality limits (#3313)
Implements metric name based cardinality limits, this does not yet use a Redis pipeline.
1 parent c1ffb07 commit 79fd7ca

File tree

11 files changed

+277
-112
lines changed

11 files changed

+277
-112
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Extract metrics from transaction spans. ([#3273](https://github.com/getsentry/relay/pull/3273), [#3324](https://github.com/getsentry/relay/pull/3324))
2020
- Implement volume metric stats. ([#3281](https://github.com/getsentry/relay/pull/3281))
2121
- Scrub transactions before enforcing quotas. ([#3248](https://github.com/getsentry/relay/pull/3248))
22+
- Implement metric name based cardinality limits. ([#3313](https://github.com/getsentry/relay/pull/3313))
2223
- Kafka topic config supports default topic names as keys. ([#3282](https://github.com/getsentry/relay/pull/3282))
2324
- Set all span tags on the transaction span. ([#3310](https://github.com/getsentry/relay/pull/3310))
2425
- Collect duration for all spans. ([#3322](https://github.com/getsentry/relay/pull/3322))

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

relay-cardinality/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ redis = ["relay-redis/impl"]
1818
workspace = true
1919

2020
[dependencies]
21+
hash32 = { workspace = true }
2122
hashbrown = { workspace = true }
2223
parking_lot = { workspace = true }
2324
relay-base-schema = { workspace = true }

relay-cardinality/benches/redis_impl.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,23 @@ impl Params {
7373
}
7474

7575
#[inline(always)]
76-
fn run(&self, limiter: &RedisSetLimiter, entries: impl IntoIterator<Item = Entry>) {
76+
fn run<'a>(&self, limiter: &RedisSetLimiter, entries: impl IntoIterator<Item = Entry<'a>>) {
7777
limiter
7878
.check_cardinality_limits(self.scoping, &self.limits, entries, &mut NoopRejections)
7979
.unwrap();
8080
}
8181

8282
/// Every round contains the same hashes.
83-
fn rounds(&self) -> Vec<Vec<Entry>> {
83+
fn rounds(&self) -> Vec<Vec<Entry<'static>>> {
8484
let entries = (0..self.num_hashes)
85-
.map(|i| Entry::new(EntryId(i), MetricNamespace::Custom, u32::MAX - (i as u32)))
85+
.map(|i| {
86+
Entry::new(
87+
EntryId(i),
88+
MetricNamespace::Custom,
89+
"foo",
90+
u32::MAX - (i as u32),
91+
)
92+
})
8693
.collect::<Vec<_>>();
8794

8895
(0..self.rounds)
@@ -91,7 +98,7 @@ impl Params {
9198
}
9299

93100
/// High cardinality, every round contains unique hashes.
94-
fn rounds_unique(&self) -> Vec<Vec<Entry>> {
101+
fn rounds_unique(&self) -> Vec<Vec<Entry<'static>>> {
95102
let hash = AtomicU32::new(u32::MAX);
96103

97104
(0..self.rounds)
@@ -101,6 +108,7 @@ impl Params {
101108
Entry::new(
102109
EntryId(i),
103110
MetricNamespace::Custom,
111+
"foo",
104112
hash.fetch_sub(1, Ordering::SeqCst),
105113
)
106114
})
@@ -110,17 +118,18 @@ impl Params {
110118
}
111119

112120
/// Entry which is never generated by either [`Self::rounds`] or [`Self::rounds_unique`].
113-
fn never_entry() -> Entry {
114-
Entry::new(EntryId(usize::MAX), MetricNamespace::Custom, 0)
121+
fn never_entry() -> Entry<'static> {
122+
Entry::new(EntryId(usize::MAX), MetricNamespace::Custom, "foo", 0)
115123
}
116124

117125
/// A vector of entries which is never generated by either [`Self::rounds`] or [`Self::rounds_unique`].
118-
fn never_entries(&self) -> Vec<Entry> {
126+
fn never_entries(&self) -> Vec<Entry<'static>> {
119127
(0..self.limits[0].limit)
120128
.map(|i| {
121129
Entry::new(
122130
EntryId(usize::MAX - i as usize),
123131
MetricNamespace::Custom,
132+
"foo",
124133
i as u32,
125134
)
126135
})

relay-cardinality/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub enum CardinalityScope {
4444
/// The limit will be enforced for a specific project.
4545
Project,
4646

47+
/// A per metric name cardinality limit.
48+
Name,
49+
4750
/// Any other scope that is not known by this Relay.
4851
#[serde(other)]
4952
Unknown,

relay-cardinality/src/limiter.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ pub trait Limiter {
3030
/// Verifies cardinality limits.
3131
///
3232
/// Returns an iterator containing only accepted entries.
33-
fn check_cardinality_limits<'a, E, R>(
33+
fn check_cardinality_limits<'a, 'b, E, R>(
3434
&self,
3535
scoping: Scoping,
3636
limits: &'a [CardinalityLimit],
3737
entries: E,
3838
rejections: &mut R,
3939
) -> Result<()>
4040
where
41-
E: IntoIterator<Item = Entry>,
41+
E: IntoIterator<Item = Entry<'b>>,
4242
R: Rejections<'a>;
4343
}
4444

@@ -51,16 +51,21 @@ pub trait CardinalityItem {
5151
///
5252
/// If this method returns `None` the item is automatically rejected.
5353
fn namespace(&self) -> Option<MetricNamespace>;
54+
55+
/// Name of the item.
56+
fn name(&self) -> &str;
5457
}
5558

5659
/// A single entry to check cardinality for.
5760
#[derive(Clone, Copy, Debug)]
58-
pub struct Entry {
61+
pub struct Entry<'a> {
5962
/// Opaque entry Id, used to keep track of indices and buckets.
6063
pub id: EntryId,
6164

62-
/// Metric namespace to which the cardinality limit is scoped.
65+
/// Metric namespace to which the cardinality limit can be scoped.
6366
pub namespace: MetricNamespace,
67+
/// Name to which the cardinality limit can be scoped.
68+
pub name: &'a str,
6469
/// Hash of the metric name and tags.
6570
pub hash: u32,
6671
}
@@ -73,12 +78,13 @@ pub struct Entry {
7378
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
7479
pub struct EntryId(pub usize);
7580

76-
impl Entry {
81+
impl<'a> Entry<'a> {
7782
/// Creates a new entry.
78-
pub fn new(id: EntryId, namespace: MetricNamespace, hash: u32) -> Self {
83+
pub fn new(id: EntryId, namespace: MetricNamespace, name: &'a str, hash: u32) -> Self {
7984
Self {
8085
id,
8186
namespace,
87+
name,
8288
hash,
8389
}
8490
}
@@ -112,7 +118,12 @@ impl<T: Limiter> CardinalityLimiter<T> {
112118

113119
metric!(timer(CardinalityLimiterTimers::CardinalityLimiter), {
114120
let entries = items.iter().enumerate().filter_map(|(id, item)| {
115-
Some(Entry::new(EntryId(id), item.namespace()?, item.to_hash()))
121+
Some(Entry::new(
122+
EntryId(id),
123+
item.namespace()?,
124+
item.name(),
125+
item.to_hash(),
126+
))
116127
});
117128

118129
let mut rejections = RejectionTracker::default();
@@ -245,6 +256,10 @@ mod tests {
245256
fn namespace(&self) -> Option<MetricNamespace> {
246257
self.namespace
247258
}
259+
260+
fn name(&self) -> &str {
261+
"foobar"
262+
}
248263
}
249264

250265
fn build_limits() -> [CardinalityLimit; 1] {
@@ -314,15 +329,15 @@ mod tests {
314329
struct RejectAllLimiter;
315330

316331
impl Limiter for RejectAllLimiter {
317-
fn check_cardinality_limits<'a, I, T>(
332+
fn check_cardinality_limits<'a, 'b, I, T>(
318333
&self,
319334
_scoping: Scoping,
320335
limits: &'a [CardinalityLimit],
321336
entries: I,
322337
rejections: &mut T,
323338
) -> Result<()>
324339
where
325-
I: IntoIterator<Item = Entry>,
340+
I: IntoIterator<Item = Entry<'b>>,
326341
T: Rejections<'a>,
327342
{
328343
for entry in entries {
@@ -356,15 +371,15 @@ mod tests {
356371
struct AcceptAllLimiter;
357372

358373
impl Limiter for AcceptAllLimiter {
359-
fn check_cardinality_limits<'a, I, T>(
374+
fn check_cardinality_limits<'a, 'b, I, T>(
360375
&self,
361376
_scoping: Scoping,
362377
_limits: &'a [CardinalityLimit],
363378
_entries: I,
364379
_rejections: &mut T,
365380
) -> Result<()>
366381
where
367-
I: IntoIterator<Item = Entry>,
382+
I: IntoIterator<Item = Entry<'b>>,
368383
T: Rejections<'a>,
369384
{
370385
Ok(())
@@ -390,15 +405,15 @@ mod tests {
390405
struct RejectEvenLimiter;
391406

392407
impl Limiter for RejectEvenLimiter {
393-
fn check_cardinality_limits<'a, I, T>(
408+
fn check_cardinality_limits<'a, 'b, I, T>(
394409
&self,
395410
scoping: Scoping,
396411
limits: &'a [CardinalityLimit],
397412
entries: I,
398413
rejections: &mut T,
399414
) -> Result<()>
400415
where
401-
I: IntoIterator<Item = Entry>,
416+
I: IntoIterator<Item = Entry<'b>>,
402417
T: Rejections<'a>,
403418
{
404419
assert_eq!(scoping, build_scoping());
@@ -445,15 +460,15 @@ mod tests {
445460
struct RejectLimits;
446461

447462
impl Limiter for RejectLimits {
448-
fn check_cardinality_limits<'a, I, T>(
463+
fn check_cardinality_limits<'a, 'b, I, T>(
449464
&self,
450465
_scoping: Scoping,
451466
limits: &'a [CardinalityLimit],
452467
entries: I,
453468
rejections: &mut T,
454469
) -> Result<()>
455470
where
456-
I: IntoIterator<Item = Entry>,
471+
I: IntoIterator<Item = Entry<'b>>,
457472
T: Rejections<'a>,
458473
{
459474
for entry in entries {

relay-cardinality/src/redis/cache.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,17 @@ impl ScopedCache {
196196

197197
#[cfg(test)]
198198
mod tests {
199+
use relay_base_schema::metrics::MetricNamespace;
199200
use relay_base_schema::project::ProjectId;
200201

202+
use crate::limiter::{Entry, EntryId};
203+
use crate::redis::quota::PartialQuotaScoping;
201204
use crate::{CardinalityLimit, CardinalityScope, OrganizationId, Scoping, SlidingWindow};
202205

203206
use super::*;
204207

205208
fn build_scoping(organization_id: OrganizationId, window: SlidingWindow) -> QuotaScoping {
206-
QuotaScoping::new(
209+
PartialQuotaScoping::new(
207210
Scoping {
208211
organization_id,
209212
project_id: ProjectId::new(1),
@@ -218,6 +221,12 @@ mod tests {
218221
},
219222
)
220223
.unwrap()
224+
.complete(Entry {
225+
id: EntryId(0),
226+
namespace: MetricNamespace::Spans,
227+
name: "foobar",
228+
hash: 123,
229+
})
221230
}
222231

223232
#[test]

0 commit comments

Comments
 (0)