From 9d4a48a070ffcf0dcf0925cd9616ae1f68e38567 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 25 Nov 2025 16:22:02 -0800 Subject: [PATCH 1/2] fix(issues): Fix `auto_ongoing_issues` task timeouts --- src/sentry/tasks/auto_ongoing_issues.py | 47 ++++++++++++++++--------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/sentry/tasks/auto_ongoing_issues.py b/src/sentry/tasks/auto_ongoing_issues.py index c79a7868bd6470..21e09107d34bd6 100644 --- a/src/sentry/tasks/auto_ongoing_issues.py +++ b/src/sentry/tasks/auto_ongoing_issues.py @@ -2,11 +2,10 @@ from datetime import datetime, timedelta, timezone import sentry_sdk -from django.db.models import Max from sentry.issues.ongoing import TRANSITION_AFTER_DAYS, bulk_transition_group_to_ongoing from sentry.models.group import Group, GroupStatus -from sentry.models.grouphistory import GroupHistoryStatus +from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import issues_tasks @@ -163,14 +162,21 @@ def get_total_count(results): nonlocal total_count total_count += len(results) - base_queryset = ( - Group.objects.filter( - status=GroupStatus.UNRESOLVED, - substatus=GroupSubStatus.REGRESSED, - grouphistory__status=GroupHistoryStatus.REGRESSED, + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + regressed_group_ids = ( + GroupHistory.objects.filter( + status=GroupHistoryStatus.REGRESSED, + date_added__lte=date_threshold, ) - .annotate(recent_regressed_history=Max("grouphistory__date_added")) - .filter(recent_regressed_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .values_list("group_id", flat=True) + .distinct() + ) + + base_queryset = Group.objects.filter( + id__in=regressed_group_ids, + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.REGRESSED, ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): @@ -244,14 +250,23 @@ def get_total_count(results): nonlocal total_count total_count += len(results) - base_queryset = ( - Group.objects.filter( - status=GroupStatus.UNRESOLVED, - substatus=GroupSubStatus.ESCALATING, - grouphistory__status=GroupHistoryStatus.ESCALATING, + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + # Query GroupHistory first to avoid expensive JOIN + MAX aggregation. + # Find groups with ESCALATING history within the date range. + escalating_group_ids = ( + GroupHistory.objects.filter( + status=GroupHistoryStatus.ESCALATING, + date_added__lte=date_threshold, ) - .annotate(recent_escalating_history=Max("grouphistory__date_added")) - .filter(recent_escalating_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .values_list("group_id", flat=True) + .distinct() + ) + + base_queryset = Group.objects.filter( + id__in=escalating_group_ids, + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.ESCALATING, ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): From 8a2bbd86db79baaa204226da9c1eb1b3c86f3ad2 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 25 Nov 2025 16:36:00 -0800 Subject: [PATCH 2/2] correct history query to find only most recent regression + test --- src/sentry/tasks/auto_ongoing_issues.py | 63 ++++++++++------- .../sentry/tasks/test_auto_ongoing_issues.py | 67 +++++++++++++++++++ 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/src/sentry/tasks/auto_ongoing_issues.py b/src/sentry/tasks/auto_ongoing_issues.py index 21e09107d34bd6..ff56e0fb5af5cb 100644 --- a/src/sentry/tasks/auto_ongoing_issues.py +++ b/src/sentry/tasks/auto_ongoing_issues.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone import sentry_sdk +from django.db.models import Max, OuterRef, Subquery from sentry.issues.ongoing import TRANSITION_AFTER_DAYS, bulk_transition_group_to_ongoing from sentry.models.group import Group, GroupStatus @@ -164,19 +165,26 @@ def get_total_count(results): date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) - regressed_group_ids = ( - GroupHistory.objects.filter( - status=GroupHistoryStatus.REGRESSED, - date_added__lte=date_threshold, - ) - .values_list("group_id", flat=True) - .distinct() + # Use a subquery to get the most recent REGRESSED history date for each group. + # This ensures we only transition groups whose MOST RECENT regressed history + # is older than the threshold, not just any regressed history. + latest_regressed_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.REGRESSED) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] ) - base_queryset = Group.objects.filter( - id__in=regressed_group_ids, - status=GroupStatus.UNRESOLVED, - substatus=GroupSubStatus.REGRESSED, + base_queryset = ( + Group.objects.filter( + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.REGRESSED, + ) + .annotate(recent_regressed_history=Subquery(latest_regressed_subquery)) + .filter( + recent_regressed_history__lte=date_threshold, + recent_regressed_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): @@ -250,23 +258,30 @@ def get_total_count(results): nonlocal total_count total_count += len(results) + from django.db.models import Max, OuterRef, Subquery + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) - # Query GroupHistory first to avoid expensive JOIN + MAX aggregation. - # Find groups with ESCALATING history within the date range. - escalating_group_ids = ( - GroupHistory.objects.filter( - status=GroupHistoryStatus.ESCALATING, - date_added__lte=date_threshold, - ) - .values_list("group_id", flat=True) - .distinct() + # Use a subquery to get the most recent ESCALATING history date for each group. + # This ensures we only transition groups whose MOST RECENT escalating history + # is older than the threshold, not just any escalating history. + latest_escalating_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.ESCALATING) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] ) - base_queryset = Group.objects.filter( - id__in=escalating_group_ids, - status=GroupStatus.UNRESOLVED, - substatus=GroupSubStatus.ESCALATING, + base_queryset = ( + Group.objects.filter( + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.ESCALATING, + ) + .annotate(recent_escalating_history=Subquery(latest_escalating_subquery)) + .filter( + recent_escalating_history__lte=date_threshold, + recent_escalating_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): diff --git a/tests/sentry/tasks/test_auto_ongoing_issues.py b/tests/sentry/tasks/test_auto_ongoing_issues.py index 63212e7bcca25e..f18db99669885c 100644 --- a/tests/sentry/tasks/test_auto_ongoing_issues.py +++ b/tests/sentry/tasks/test_auto_ongoing_issues.py @@ -345,6 +345,73 @@ def test_not_all_groups_get_updated(self, mock_metrics_incr) -> None: tags={"count": 0}, ) + @freeze_time("2023-07-12 18:40:00Z") + def test_only_checks_most_recent_regressed_history(self) -> None: + """ + Test that only the MOST RECENT regressed history is checked against the threshold, + not just any regressed history. + + Scenario: + - Group regressed 14 days ago (older than 7-day threshold) + - Group resolved 10 days ago + - Group regressed again 2 days ago (newer than 7-day threshold) + + Expected: Group should NOT be transitioned because most recent regression is only 2 days old + """ + now = datetime.now(tz=timezone.utc) + project = self.create_project() + group = self.create_group( + project=project, + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.REGRESSED, + first_seen=now - timedelta(days=14), + ) + + # Create OLD regressed history (14 days ago) - this is OLDER than threshold + old_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + old_regressed_history.date_added = now - timedelta(days=14) + old_regressed_history.save(update_fields=["date_added"]) + + # Create resolved history in between (10 days ago) + resolved_history = record_group_history( + group, GroupHistoryStatus.RESOLVED, actor=None, release=None + ) + resolved_history.date_added = now - timedelta(days=10) + resolved_history.save(update_fields=["date_added"]) + + # Create NEW regressed history (2 days ago) - this is NEWER than threshold + # This is the MOST RECENT regressed history + new_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + new_regressed_history.date_added = now - timedelta(days=2) + new_regressed_history.save(update_fields=["date_added"]) + + # Also create a recent group inbox entry + group_inbox = add_group_to_inbox(group, GroupInboxReason.REGRESSION) + group_inbox.date_added = now - timedelta(days=2) + group_inbox.save(update_fields=["date_added"]) + + with self.tasks(): + schedule_auto_transition_to_ongoing() + + # Group should NOT be transitioned because most recent regression is only 2 days old + group.refresh_from_db() + assert group.status == GroupStatus.UNRESOLVED + assert group.substatus == GroupSubStatus.REGRESSED # Should still be REGRESSED + + # Should NOT have created an auto-ongoing activity + assert not Activity.objects.filter( + group=group, type=ActivityType.AUTO_SET_ONGOING.value + ).exists() + + # Should NOT have created an ONGOING history entry + assert not GroupHistory.objects.filter( + group=group, status=GroupHistoryStatus.ONGOING + ).exists() + class ScheduleAutoEscalatingOngoingIssuesTest(TestCase): @freeze_time("2023-07-12 18:40:00Z")