Skip to content

Commit 21ffa6d

Browse files
scttcperclaudegetsantry[bot]
authored
perf(issues): Reuse the group when fetching escalation forecasts (#127719)
Avoid reloading the group just to check `should_detect_escalation()` when fetching an escalation forecast, both callers already have it. The lookup was left over from when the check took an organization behind a flag (#70443). This runs for events on issues archived until escalating, roughly 30M times a week in post_process at ~2ms p50, around 17 hours of db time a week (ballpark) so again a pretty small change. [Example trace](https://sentry.sentry.io/explore/traces/trace/d1138b8a7a8248d4b28a7f349b10225f/?node=span-91f7b1de6e7f52a1), the `sentry_groupedmessage` select under `process_snoozes` is the one that goes away. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 2ddac13 commit 21ffa6d

5 files changed

Lines changed: 22 additions & 44 deletions

File tree

‎src/sentry/issues/endpoints/group_details.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def get(self, request: Request, group: Group) -> Response[GroupDetailsResponse]:
306306
data.update({"owners": owners})
307307

308308
if "forecast" in expand:
309-
fetched_forecast = EscalatingGroupForecast.fetch(group.project_id, group.id)
309+
fetched_forecast = EscalatingGroupForecast.fetch(group)
310310
if fetched_forecast:
311311
fetched_forecast_dict = fetched_forecast.to_dict()
312312
data.update(

‎src/sentry/issues/escalating/escalating.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ def is_escalating(group: Group) -> tuple[bool, int | None]:
472472
},
473473
)
474474

475-
forecast_today = EscalatingGroupForecast.fetch_todays_forecast(group.project.id, group.id)
475+
forecast_today = EscalatingGroupForecast.fetch_todays_forecast(group)
476476
# Check if current event occurrence is greater than forecast for today's date
477477
if forecast_today and group_hourly_count > forecast_today:
478478
return True, forecast_today

‎src/sentry/issues/escalating/escalating_group_forecast.py‎

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,7 @@ def save(self) -> None:
4949
)
5050

5151
@classmethod
52-
def _should_fetch_escalating(cls, group_id: int) -> bool:
53-
group = Group.objects.get(id=group_id)
54-
55-
return group.issue_type.should_detect_escalation()
56-
57-
@classmethod
58-
def fetch(cls, project_id: int, group_id: int) -> EscalatingGroupForecast | None:
52+
def fetch(cls, group: Group) -> EscalatingGroupForecast | None:
5953
"""
6054
Return the forecast from nodestore if it exists.
6155
@@ -66,24 +60,24 @@ def fetch(cls, project_id: int, group_id: int) -> EscalatingGroupForecast | None
6660
"""
6761
from sentry.issues.escalating.forecasts import generate_and_save_missing_forecasts
6862

69-
if not cls._should_fetch_escalating(group_id=group_id):
63+
if not group.issue_type.should_detect_escalation():
7064
return None
7165

72-
results = nodestore.backend.get(cls.build_storage_identifier(project_id, group_id))
66+
results = nodestore.backend.get(cls.build_storage_identifier(group.project_id, group.id))
7367
if results:
7468
return EscalatingGroupForecast.from_dict(results)
75-
generate_and_save_missing_forecasts.delay(group_id=group_id)
69+
generate_and_save_missing_forecasts.delay(group_id=group.id)
7670
return EscalatingGroupForecast(
77-
project_id=project_id,
78-
group_id=group_id,
71+
project_id=group.project_id,
72+
group_id=group.id,
7973
forecast=ONE_EVENT_FORECAST,
8074
date_added=datetime.now(),
8175
)
8276

8377
@classmethod
84-
def fetch_todays_forecast(cls, project_id: int, group_id: int) -> int | None:
78+
def fetch_todays_forecast(cls, group: Group) -> int | None:
8579
date_now = datetime.now().date()
86-
escalating_forecast = EscalatingGroupForecast.fetch(project_id, group_id)
80+
escalating_forecast = EscalatingGroupForecast.fetch(group)
8781

8882
if not escalating_forecast:
8983
return None

‎tests/sentry/issues/test_ignored.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_archive_until_escalating_no_counts(
7676
assert not GroupSnooze.objects.filter(group=self.group).exists()
7777
assert status_details == {"ignoreUntilEscalating": True}
7878

79-
fetched_forecast = EscalatingGroupForecast.fetch(self.group.project.id, self.group.id)
79+
fetched_forecast = EscalatingGroupForecast.fetch(self.group)
8080
assert fetched_forecast and fetched_forecast.forecast == ONE_EVENT_FORECAST
8181
assert mock_generate_and_save_missing_forecasts.call_count == 1
8282

@@ -103,7 +103,7 @@ def test_archive_until_escalating_with_counts(
103103
assert not GroupSnooze.objects.filter(group=self.group).exists()
104104
assert status_details == {"ignoreUntilEscalating": True}
105105

106-
fetched_forecast = EscalatingGroupForecast.fetch(self.group.project.id, self.group.id)
106+
fetched_forecast = EscalatingGroupForecast.fetch(self.group)
107107
assert fetched_forecast is not None
108108
assert fetched_forecast.project_id == self.group.project.id
109109
assert fetched_forecast.group_id == self.group.id

‎tests/sentry/tasks/test_weekly_escalating_forecast.py‎

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_empty_escalating_forecast(
4848

4949
run_escalating_forecast()
5050
group = group_list[0]
51-
fetched_forecast = EscalatingGroupForecast.fetch(group.project.id, group.id)
51+
fetched_forecast = EscalatingGroupForecast.fetch(group)
5252
assert fetched_forecast and fetched_forecast.forecast == ONE_EVENT_FORECAST
5353
assert mock_generate_and_save_missing_forecasts.call_count == 1
5454

@@ -71,7 +71,7 @@ def test_empty_sd_escalating_forecast(
7171

7272
run_escalating_forecast()
7373
group = group_list[0]
74-
fetched_forecast = EscalatingGroupForecast.fetch(group.project.id, group.id)
74+
fetched_forecast = EscalatingGroupForecast.fetch(group)
7575
assert fetched_forecast is None
7676
assert mock_generate_and_save_missing_forecasts.call_count == 0
7777

@@ -91,9 +91,7 @@ def test_single_group_escalating_forecast(
9191

9292
run_escalating_forecast()
9393
approximate_date_added = datetime.now(timezone.utc)
94-
fetched_forecast = EscalatingGroupForecast.fetch(
95-
group_list[0].project.id, group_list[0].id
96-
)
94+
fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
9795
assert fetched_forecast is not None
9896
assert fetched_forecast.project_id == group_list[0].project.id
9997
assert fetched_forecast.group_id == group_list[0].id
@@ -125,9 +123,7 @@ def test_single_sd_group_escalating_forecast(
125123

126124
run_escalating_forecast()
127125

128-
fetched_forecast = EscalatingGroupForecast.fetch(
129-
group_list[0].project.id, group_list[0].id
130-
)
126+
fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
131127

132128
assert fetched_forecast is None
133129
self.assertNotIn("issue_forecasts.saved", record_mock.call_args)
@@ -149,9 +145,7 @@ def test_multiple_groups_escalating_forecast(
149145
run_escalating_forecast()
150146
approximate_date_added = datetime.now(timezone.utc)
151147
for i in range(len(group_list)):
152-
fetched_forecast = EscalatingGroupForecast.fetch(
153-
group_list[i].project.id, group_list[i].id
154-
)
148+
fetched_forecast = EscalatingGroupForecast.fetch(group_list[i])
155149
assert fetched_forecast is not None
156150
assert fetched_forecast.project_id == group_list[i].project.id
157151
assert fetched_forecast.group_id == group_list[i].id
@@ -183,9 +177,7 @@ def test_multiple_sd_groups_escalating_forecast(
183177

184178
run_escalating_forecast()
185179
for i in range(len(group_list)):
186-
fetched_forecast = EscalatingGroupForecast.fetch(
187-
group_list[i].project.id, group_list[i].id
188-
)
180+
fetched_forecast = EscalatingGroupForecast.fetch(group_list[i])
189181
assert fetched_forecast is None
190182

191183
self.assertNotIn("issue_forecasts.saved", record_mock.call_args)
@@ -205,15 +197,11 @@ def test_update_group_escalating_forecast(
205197
)
206198

207199
run_escalating_forecast()
208-
first_fetched_forecast = EscalatingGroupForecast.fetch(
209-
group_list[0].project.id, group_list[0].id
210-
)
200+
first_fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
211201

212202
# Assert update when this is run twice
213203
run_escalating_forecast()
214-
second_fetched_forecast = EscalatingGroupForecast.fetch(
215-
group_list[0].project.id, group_list[0].id
216-
)
204+
second_fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
217205
assert first_fetched_forecast is not None
218206
assert second_fetched_forecast is not None
219207
assert first_fetched_forecast.date_added < second_fetched_forecast.date_added
@@ -239,15 +227,11 @@ def test_update_sd_group_escalating_forecast(
239227
)
240228

241229
run_escalating_forecast()
242-
first_fetched_forecast = EscalatingGroupForecast.fetch(
243-
group_list[0].project.id, group_list[0].id
244-
)
230+
first_fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
245231

246232
# Assert update when this is run twice
247233
run_escalating_forecast()
248-
second_fetched_forecast = EscalatingGroupForecast.fetch(
249-
group_list[0].project.id, group_list[0].id
250-
)
234+
second_fetched_forecast = EscalatingGroupForecast.fetch(group_list[0])
251235
assert first_fetched_forecast is None
252236
assert second_fetched_forecast is None
253237

0 commit comments

Comments
 (0)