Skip to content

Commit e33f7b6

Browse files
authored
fix(aci): fix Group N+1 query in delayed workflow query handlers (#95161)
1 parent 70438a5 commit e33f7b6

File tree

4 files changed

+77
-69
lines changed

4 files changed

+77
-69
lines changed

src/sentry/workflow_engine/handlers/condition/event_frequency_query_handlers.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import Any, ClassVar, Literal, Protocol, TypedDict
77

88
from django.core.cache import cache
9-
from django.db.models import QuerySet
109
from snuba_sdk import Op
1110

1211
from sentry import release_health, tsdb
@@ -16,7 +15,6 @@
1615
get_issue_tsdb_user_group_model,
1716
)
1817
from sentry.issues.grouptype import GroupCategory, get_group_type_by_type_id
19-
from sentry.models.group import Group
2018
from sentry.rules.conditions.event_attribute import ATTR_CHOICES
2119
from sentry.rules.conditions.event_frequency import (
2220
MIN_SESSIONS_TO_FIRE,
@@ -35,6 +33,13 @@
3533
QueryResult = dict[int, int | float]
3634

3735

36+
class GroupValues(TypedDict):
37+
id: int
38+
type: int
39+
project_id: int
40+
project__organization_id: int
41+
42+
3843
class TSDBFunction(Protocol):
3944
def __call__(
4045
self,
@@ -61,13 +66,6 @@ class InvalidFilter(Exception):
6166
pass
6267

6368

64-
class _QSTypedDict(TypedDict):
65-
id: int
66-
type: int
67-
project_id: int
68-
project__organization_id: int
69-
70-
7169
class BaseEventFrequencyQueryHandler(ABC):
7270
intervals: ClassVar[dict[str, tuple[str, timedelta]]] = STANDARD_INTERVALS
7371

@@ -154,7 +152,7 @@ def get_chunked_result(
154152

155153
def get_group_ids_by_category(
156154
self,
157-
groups: QuerySet[Group, _QSTypedDict],
155+
groups: list[GroupValues],
158156
) -> dict[GroupCategory, list[int]]:
159157
"""
160158
Separate group ids into error group ids and generic group ids
@@ -170,7 +168,7 @@ def get_group_ids_by_category(
170168

171169
def get_value_from_groups(
172170
self,
173-
groups: QuerySet[Group, _QSTypedDict] | None,
171+
groups: list[GroupValues],
174172
value: Literal["id", "project_id", "project__organization_id"],
175173
) -> int | None:
176174
result = None
@@ -270,7 +268,7 @@ def convert_filter_to_snuba_condition(
270268
@abstractmethod
271269
def batch_query(
272270
self,
273-
group_ids: set[int],
271+
groups: list[GroupValues],
274272
start: datetime,
275273
end: datetime,
276274
environment_id: int | None,
@@ -285,7 +283,7 @@ def batch_query(
285283
def get_rate_bulk(
286284
self,
287285
duration: timedelta,
288-
group_ids: set[int],
286+
groups: list[GroupValues],
289287
environment_id: int | None,
290288
current_time: datetime,
291289
comparison_interval: timedelta | None,
@@ -306,7 +304,7 @@ def get_rate_bulk(
306304

307305
with self.disable_consistent_snuba_mode(duration):
308306
result = self.batch_query(
309-
group_ids=group_ids,
307+
groups=groups,
310308
start=start,
311309
end=end,
312310
environment_id=environment_id,
@@ -325,16 +323,13 @@ def get_rate_bulk(
325323
class EventFrequencyQueryHandler(BaseEventFrequencyQueryHandler):
326324
def batch_query(
327325
self,
328-
group_ids: set[int],
326+
groups: list[GroupValues],
329327
start: datetime,
330328
end: datetime,
331329
environment_id: int | None,
332330
filters: list[QueryFilter] | None = None,
333331
) -> QueryResult:
334332
batch_sums: QueryResult = defaultdict(int)
335-
groups = Group.objects.filter(id__in=group_ids).values(
336-
"id", "type", "project_id", "project__organization_id"
337-
)
338333
category_group_ids = self.get_group_ids_by_category(groups)
339334
organization_id = self.get_value_from_groups(groups, "project__organization_id")
340335

@@ -371,16 +366,13 @@ def batch_query(
371366
class EventUniqueUserFrequencyQueryHandler(BaseEventFrequencyQueryHandler):
372367
def batch_query(
373368
self,
374-
group_ids: set[int],
369+
groups: list[GroupValues],
375370
start: datetime,
376371
end: datetime,
377372
environment_id: int | None,
378373
filters: list[QueryFilter] | None = None,
379374
) -> QueryResult:
380375
batch_sums: QueryResult = defaultdict(int)
381-
groups = Group.objects.filter(id__in=group_ids).values(
382-
"id", "type", "project_id", "project__organization_id"
383-
)
384376
category_group_ids = self.get_group_ids_by_category(groups)
385377
organization_id = self.get_value_from_groups(groups, "project__organization_id")
386378

@@ -442,16 +434,13 @@ def get_session_interval(self, session_count: int, duration: timedelta) -> int |
442434

443435
def batch_query(
444436
self,
445-
group_ids: set[int],
437+
groups: list[GroupValues],
446438
start: datetime,
447439
end: datetime,
448440
environment_id: int | None,
449441
filters: list[QueryFilter] | None = None,
450442
) -> QueryResult:
451443
batch_percents: QueryResult = {}
452-
groups = Group.objects.filter(id__in=group_ids).values(
453-
"id", "type", "project_id", "project__organization_id"
454-
)
455444
category_group_ids = self.get_group_ids_by_category(groups)
456445
project_id = self.get_value_from_groups(groups, "project_id")
457446

src/sentry/workflow_engine/processors/delayed_workflow.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from sentry.utils.retries import ConditionalRetryPolicy, exponential_delay
4040
from sentry.workflow_engine.handlers.condition.event_frequency_query_handlers import (
4141
BaseEventFrequencyQueryHandler,
42+
GroupValues,
4243
QueryFilter,
4344
QueryResult,
4445
slow_condition_query_handler_registry,
@@ -423,9 +424,21 @@ def get_condition_group_results(
423424
condition_group_results = {}
424425
current_time = timezone.now()
425426

427+
all_group_ids: set[GroupId] = set()
428+
# bulk gather groups and fetch them
429+
for time_and_groups in queries_to_groups.values():
430+
all_group_ids.update(time_and_groups.group_ids)
431+
432+
all_groups: list[GroupValues] = list(
433+
Group.objects.filter(id__in=all_group_ids).values(
434+
"id", "type", "project_id", "project__organization_id"
435+
)
436+
)
437+
426438
for unique_condition, time_and_groups in queries_to_groups.items():
427439
handler = unique_condition.handler()
428440
group_ids = time_and_groups.group_ids
441+
groups_to_query = [group for group in all_groups if group["id"] in group_ids]
429442
time = time_and_groups.timestamp or current_time
430443

431444
_, duration = handler.intervals[unique_condition.interval]
@@ -438,7 +451,7 @@ def get_condition_group_results(
438451

439452
result = handler.get_rate_bulk(
440453
duration=duration,
441-
group_ids=group_ids,
454+
groups=groups_to_query,
442455
environment_id=unique_condition.environment_id,
443456
current_time=time,
444457
comparison_interval=comparison_interval,

tests/sentry/workflow_engine/handlers/condition/test_base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from uuid import uuid4
44

55
from sentry.issues.grouptype import PerformanceNPlusOneGroupType
6+
from sentry.models.group import Group
67
from sentry.testutils.cases import PerformanceIssueTestCase, RuleTestCase, SnubaTestCase
78
from sentry.testutils.helpers.datetime import before_now
89
from sentry.utils.samples import load_data
@@ -129,3 +130,14 @@ def setUp(self):
129130
fingerprint=fingerprint,
130131
)
131132
self.data = {"interval": "5m", "value": 30}
133+
134+
self.groups = list(
135+
Group.objects.filter(
136+
id__in={self.event.group_id, self.event2.group_id, self.perf_event.group_id}
137+
).values("id", "type", "project_id", "project__organization_id")
138+
)
139+
self.group_3 = list(
140+
Group.objects.filter(id=self.event3.group_id).values(
141+
"id", "type", "project_id", "project__organization_id"
142+
)
143+
)

0 commit comments

Comments
 (0)