Skip to content

Commit 4487c7b

Browse files
jjbayerDav1dde
andauthored
fix(quota): Revert application of indexed quota on nested spans (#4108)
Reverts #4097. Never really worked, relied on a bug in fast-path rate limiting to even pass the tests. --------- Co-authored-by: David Herberth <[email protected]>
1 parent 433f59c commit 4487c7b

File tree

10 files changed

+131
-225
lines changed

10 files changed

+131
-225
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ jobs:
496496
test_integration:
497497
name: Integration Tests
498498
runs-on: ubuntu-latest
499-
timeout-minutes: 20
499+
timeout-minutes: 30
500500

501501
# Skip redundant checks for library releases
502502
if: "!startsWith(github.ref, 'refs/heads/release-library/')"

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
- Use custom wildcard matching instead of regular expressions. ([#4073](https://github.com/getsentry/relay/pull/4073))
3131
- Allowlist the SentryUptimeBot user-agent. ([#4068](https://github.com/getsentry/relay/pull/4068))
3232
- Feature flags of graduated features are now hard-coded in Relay so they can be removed from Sentry. ([#4076](https://github.com/getsentry/relay/pull/4076), [#4080](https://github.com/getsentry/relay/pull/4080))
33-
- Prevent span extraction when quota is active to reduce load on redis. ([#4097](https://github.com/getsentry/relay/pull/4097))
3433

3534
## 24.9.0
3635

relay-server/src/envelope.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ use smallvec::SmallVec;
5353

5454
use crate::constants::DEFAULT_EVENT_RETENTION;
5555
use crate::extractors::{PartialMeta, RequestMeta};
56-
use crate::utils::SeqCount;
5756

5857
pub const CONTENT_TYPE: &str = "application/x-sentry-envelope";
5958

@@ -872,27 +871,6 @@ impl Item {
872871
self.headers.other.insert(name.into(), value.into())
873872
}
874873

875-
/// Counts how many spans are contained in a transaction payload.
876-
///
877-
/// The transaction itself represents a span as well, so this function returns
878-
/// `len(event.spans) + 1`.
879-
///
880-
/// Returns zero if
881-
/// - the item is not a transaction,
882-
/// - the spans have already been extracted (in which case they are represented elsewhere).
883-
pub fn count_nested_spans(&self) -> usize {
884-
#[derive(Debug, Deserialize)]
885-
struct PartialEvent {
886-
spans: SeqCount,
887-
}
888-
889-
if self.ty() != &ItemType::Transaction || self.spans_extracted() {
890-
return 0;
891-
}
892-
893-
serde_json::from_slice::<PartialEvent>(&self.payload()).map_or(0, |event| event.spans.0 + 1)
894-
}
895-
896874
/// Determines whether the given item creates an event.
897875
///
898876
/// This is only true for literal events and crash report attachments.

relay-server/src/metrics_extraction/event.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ impl Extractable for Span {
4949
/// If this is a transaction event with spans, metrics will also be extracted from the spans.
5050
pub fn extract_metrics(
5151
event: &mut Event,
52+
spans_extracted: bool,
5253
config: CombinedMetricExtractionConfig<'_>,
5354
max_tag_value_size: usize,
5455
span_extraction_sample_rate: Option<f32>,
5556
) -> Vec<Bucket> {
5657
let mut metrics = generic::extract_metrics(event, config);
57-
if sample(span_extraction_sample_rate.unwrap_or(1.0)) {
58+
// If spans were already extracted for an event, we rely on span processing to extract metrics.
59+
if !spans_extracted && sample(span_extraction_sample_rate.unwrap_or(1.0)) {
5860
extract_span_metrics_for_event(event, config, max_tag_value_size, &mut metrics);
5961
}
6062

@@ -1200,6 +1202,7 @@ mod tests {
12001202

12011203
extract_metrics(
12021204
event.value_mut().as_mut().unwrap(),
1205+
false,
12031206
combined_config(features, None).combined(),
12041207
200,
12051208
None,
@@ -1410,6 +1413,7 @@ mod tests {
14101413

14111414
let metrics = extract_metrics(
14121415
event.value_mut().as_mut().unwrap(),
1416+
false,
14131417
combined_config([Feature::ExtractCommonSpanMetricsFromEvent], None).combined(),
14141418
200,
14151419
None,
@@ -1466,6 +1470,7 @@ mod tests {
14661470

14671471
let metrics = extract_metrics(
14681472
event.value_mut().as_mut().unwrap(),
1473+
false,
14691474
combined_config([Feature::ExtractCommonSpanMetricsFromEvent], None).combined(),
14701475
200,
14711476
None,
@@ -1497,6 +1502,7 @@ mod tests {
14971502

14981503
let metrics = extract_metrics(
14991504
event.value_mut().as_mut().unwrap(),
1505+
false,
15001506
combined_config([Feature::ExtractCommonSpanMetricsFromEvent], None).combined(),
15011507
200,
15021508
None,
@@ -1759,6 +1765,7 @@ mod tests {
17591765

17601766
let metrics = extract_metrics(
17611767
event.value_mut().as_mut().unwrap(),
1768+
false,
17621769
combined_config([Feature::ExtractCommonSpanMetricsFromEvent], None).combined(),
17631770
200,
17641771
None,
@@ -1899,7 +1906,13 @@ mod tests {
18991906
);
19001907
let config = binding.combined();
19011908

1902-
let _ = extract_metrics(event.value_mut().as_mut().unwrap(), config, 200, None);
1909+
let _ = extract_metrics(
1910+
event.value_mut().as_mut().unwrap(),
1911+
false,
1912+
config,
1913+
200,
1914+
None,
1915+
);
19031916

19041917
insta::assert_debug_snapshot!(&event.value().unwrap()._metrics_summary);
19051918
insta::assert_debug_snapshot!(

relay-server/src/services/processor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,7 @@ impl EnvelopeProcessorService {
14311431

14321432
let metrics = crate::metrics_extraction::event::extract_metrics(
14331433
event,
1434+
state.spans_extracted,
14341435
combined_config,
14351436
self.inner
14361437
.config

relay-server/src/services/project.rs

Lines changed: 80 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ use relay_system::{Addr, BroadcastChannel};
1212
use serde::{Deserialize, Serialize};
1313
use tokio::time::Instant;
1414

15+
use crate::envelope::ItemType;
1516
use crate::services::metrics::{Aggregator, MergeBuckets};
1617
use crate::services::outcome::{DiscardReason, Outcome};
1718
use crate::services::processor::{EncodeMetricMeta, EnvelopeProcessor, ProcessProjectMetrics};
1819
use crate::services::project::state::ExpiryState;
1920
use crate::services::project_cache::{
2021
CheckedEnvelope, ProcessMetrics, ProjectCache, RequestUpdate,
2122
};
23+
use crate::utils::{Enforcement, SeqCount};
2224

2325
use crate::statsd::RelayCounters;
2426
use crate::utils::{EnvelopeLimiter, ManagedEnvelope, RetryBackoff};
@@ -555,9 +557,19 @@ impl Project {
555557
Ok(current_limits.check_with_quotas(quotas, item_scoping))
556558
});
557559

558-
let (enforcement, mut rate_limits) =
560+
let (mut enforcement, mut rate_limits) =
559561
envelope_limiter.compute(envelope.envelope_mut(), &scoping)?;
560562

563+
let check_nested_spans = state
564+
.as_ref()
565+
.is_some_and(|s| s.has_feature(Feature::ExtractSpansFromEvent));
566+
567+
// If we can extract spans from the event, we want to try and count the number of nested
568+
// spans to correctly emit negative outcomes in case the transaction itself is dropped.
569+
if check_nested_spans {
570+
sync_spans_to_enforcement(&envelope, &mut enforcement);
571+
}
572+
561573
enforcement.apply_with_outcomes(&mut envelope);
562574

563575
envelope.update();
@@ -586,9 +598,52 @@ impl Project {
586598
}
587599
}
588600

601+
/// Adds category limits for the nested spans inside a transaction.
602+
///
603+
/// On the fast path of rate limiting, we do not have nested spans of a transaction extracted
604+
/// as top-level spans, thus if we limited a transaction, we want to count and emit negative
605+
/// outcomes for each of the spans nested inside that transaction.
606+
fn sync_spans_to_enforcement(envelope: &ManagedEnvelope, enforcement: &mut Enforcement) {
607+
if !enforcement.is_event_active() {
608+
return;
609+
}
610+
611+
let spans_count = count_nested_spans(envelope);
612+
if spans_count == 0 {
613+
return;
614+
}
615+
616+
if enforcement.event.is_active() {
617+
enforcement.spans = enforcement.event.clone_for(DataCategory::Span, spans_count);
618+
}
619+
620+
if enforcement.event_indexed.is_active() {
621+
enforcement.spans_indexed = enforcement
622+
.event_indexed
623+
.clone_for(DataCategory::SpanIndexed, spans_count);
624+
}
625+
}
626+
627+
/// Counts the nested spans inside the first transaction envelope item inside the [`Envelope`](crate::envelope::Envelope).
628+
fn count_nested_spans(envelope: &ManagedEnvelope) -> usize {
629+
#[derive(Debug, Deserialize)]
630+
struct PartialEvent {
631+
spans: SeqCount,
632+
}
633+
634+
envelope
635+
.envelope()
636+
.items()
637+
.find(|item| *item.ty() == ItemType::Transaction && !item.spans_extracted())
638+
.and_then(|item| serde_json::from_slice::<PartialEvent>(&item.payload()).ok())
639+
// We do + 1, since we count the transaction itself because it will be extracted
640+
// as a span and counted during the slow path of rate limiting.
641+
.map_or(0, |event| event.spans.0 + 1)
642+
}
643+
589644
#[cfg(test)]
590645
mod tests {
591-
use crate::envelope::{ContentType, Envelope, Item, ItemType};
646+
use crate::envelope::{ContentType, Envelope, Item};
592647
use crate::extractors::RequestMeta;
593648
use crate::services::processor::ProcessingGroup;
594649
use relay_base_schema::project::ProjectId;
@@ -720,7 +775,27 @@ mod tests {
720775
RequestMeta::new(dsn)
721776
}
722777

723-
const EVENT_WITH_SPANS: &str = r#"{
778+
#[test]
779+
fn test_track_nested_spans_outcomes() {
780+
let mut project = create_project(Some(json!({
781+
"features": [
782+
"organizations:indexed-spans-extraction"
783+
],
784+
"quotas": [{
785+
"id": "foo",
786+
"categories": ["transaction"],
787+
"window": 3600,
788+
"limit": 0,
789+
"reasonCode": "foo",
790+
}]
791+
})));
792+
793+
let mut envelope = Envelope::from_request(Some(EventId::new()), request_meta());
794+
795+
let mut transaction = Item::new(ItemType::Transaction);
796+
transaction.set_payload(
797+
ContentType::Json,
798+
r#"{
724799
"event_id": "52df9022835246eeb317dbd739ccd059",
725800
"type": "transaction",
726801
"transaction": "I have a stale timestamp, but I'm recent!",
@@ -746,27 +821,8 @@ mod tests {
746821
"trace_id": "ff62a8b040f340bda5d830223def1d81"
747822
}
748823
]
749-
}"#;
750-
751-
#[test]
752-
fn test_track_nested_spans_outcomes() {
753-
let mut project = create_project(Some(json!({
754-
"features": [
755-
"organizations:indexed-spans-extraction"
756-
],
757-
"quotas": [{
758-
"id": "foo",
759-
"categories": ["transaction"],
760-
"window": 3600,
761-
"limit": 0,
762-
"reasonCode": "foo",
763-
}]
764-
})));
765-
766-
let mut envelope = Envelope::from_request(Some(EventId::new()), request_meta());
767-
768-
let mut transaction = Item::new(ItemType::Transaction);
769-
transaction.set_payload(ContentType::Json, EVENT_WITH_SPANS);
824+
}"#,
825+
);
770826

771827
envelope.add_item(transaction);
772828

@@ -796,59 +852,4 @@ mod tests {
796852
assert_eq!(outcome.quantity, expected_quantity);
797853
}
798854
}
799-
800-
#[test]
801-
fn test_track_nested_spans_outcomes_span_quota() {
802-
let mut project = create_project(Some(json!({
803-
"features": [
804-
"organizations:indexed-spans-extraction"
805-
],
806-
"quotas": [{
807-
"id": "foo",
808-
"categories": ["span_indexed"],
809-
"window": 3600,
810-
"limit": 0,
811-
"reasonCode": "foo",
812-
}]
813-
})));
814-
815-
let mut envelope = Envelope::from_request(Some(EventId::new()), request_meta());
816-
817-
let mut transaction = Item::new(ItemType::Transaction);
818-
transaction.set_payload(ContentType::Json, EVENT_WITH_SPANS);
819-
820-
envelope.add_item(transaction);
821-
822-
let (outcome_aggregator, mut outcome_aggregator_rx) = Addr::custom();
823-
let (test_store, _) = Addr::custom();
824-
825-
let managed_envelope = ManagedEnvelope::new(
826-
envelope,
827-
outcome_aggregator.clone(),
828-
test_store,
829-
ProcessingGroup::Transaction,
830-
);
831-
832-
let CheckedEnvelope {
833-
envelope,
834-
rate_limits: _,
835-
} = project.check_envelope(managed_envelope).unwrap();
836-
let envelope = envelope.unwrap();
837-
let transaction_item = envelope
838-
.envelope()
839-
.items()
840-
.find(|i| *i.ty() == ItemType::Transaction)
841-
.unwrap();
842-
assert!(transaction_item.spans_extracted());
843-
844-
drop(outcome_aggregator);
845-
846-
let expected = [(DataCategory::SpanIndexed, 3)];
847-
848-
for (expected_category, expected_quantity) in expected {
849-
let outcome = outcome_aggregator_rx.blocking_recv().unwrap();
850-
assert_eq!(outcome.category, expected_category);
851-
assert_eq!(outcome.quantity, expected_quantity);
852-
}
853-
}
854855
}

relay-server/src/utils/rate_limits.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,6 @@ impl EnvelopeSummary {
217217
summary.profile_quantity += source_quantities.profiles;
218218
}
219219

220-
// Also count nested spans:
221-
summary.span_quantity += item.count_nested_spans();
222-
223220
summary.payload_size += item.len();
224221
summary.set_quantity(item);
225222
}
@@ -451,7 +448,6 @@ impl Enforcement {
451448
envelope
452449
.envelope_mut()
453450
.retain_items(|item| self.retain_item(item));
454-
455451
self.track_outcomes(envelope);
456452
}
457453

@@ -462,12 +458,6 @@ impl Enforcement {
462458
return false;
463459
}
464460

465-
if item.ty() == &ItemType::Transaction && self.spans_indexed.is_active() {
466-
// We cannot remove nested spans from the transaction, but we can prevent them
467-
// from being extracted into standalone spans.
468-
item.set_spans_extracted(true);
469-
}
470-
471461
// When checking limits for categories that have an indexed variant,
472462
// we only have to check the more specific, the indexed, variant
473463
// to determine whether an item is limited.

tests/integration/test_metrics.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,21 +1206,19 @@ def test_no_transaction_metrics_when_filtered(mini_sentry, relay):
12061206
relay = relay(mini_sentry, options=TEST_CONFIG)
12071207
relay.send_transaction(project_id, tx)
12081208

1209-
# The only envelopes received should be outcomes for {Span,Transaction}[Indexed]?:
1210-
reports = [mini_sentry.get_client_report() for _ in range(4)]
1209+
# The only envelopes received should be outcomes for Transaction{,Indexed}:
1210+
reports = [mini_sentry.get_client_report() for _ in range(2)]
12111211
filtered_events = [
12121212
outcome for report in reports for outcome in report["filtered_events"]
12131213
]
12141214
filtered_events.sort(key=lambda x: x["category"])
12151215

12161216
assert filtered_events == [
1217-
{"reason": "release-version", "category": "span", "quantity": 2},
1218-
{"reason": "release-version", "category": "span_indexed", "quantity": 2},
12191217
{"reason": "release-version", "category": "transaction", "quantity": 1},
12201218
{"reason": "release-version", "category": "transaction_indexed", "quantity": 1},
12211219
]
12221220

1223-
assert mini_sentry.captured_events.empty
1221+
assert mini_sentry.captured_events.empty()
12241222

12251223

12261224
def test_transaction_name_too_long(

0 commit comments

Comments
 (0)