Skip to content

Commit 820c80d

Browse files
authored
fix(deletions): Prevent DB call from deleting issue (#95915)
When deleting an issue, a failure to query the DB would prevent scheduling the group deletion task. From the customer's perspective, it would look that the group has been deleted, however, all relationships would still be left behind. The biggest problem would be that the group hashes are left behind, thus, any new events matching those group hashes would not create a new issue. This is a better fix than the one proposed in #93541. Fixes #93509. Fixes [ID-716](https://linear.app/getsentry/issue/ID-716/)
1 parent 79df9fa commit 820c80d

File tree

5 files changed

+57
-18
lines changed

5 files changed

+57
-18
lines changed

src/sentry/api/helpers/group_index/delete.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def delete_group_list(
7373
},
7474
)
7575

76+
# Tell seer to delete grouping records for these groups
77+
may_schedule_task_to_delete_hashes_from_seer(error_ids)
78+
7679
Group.objects.filter(id__in=group_ids).exclude(
7780
status__in=[GroupStatus.PENDING_DELETION, GroupStatus.DELETION_IN_PROGRESS]
7881
).update(status=GroupStatus.PENDING_DELETION, substatus=None)
@@ -84,9 +87,6 @@ def delete_group_list(
8487
# fails, we will still have a record of who requested the deletion.
8588
create_audit_entries(request, project, group_list, delete_type, transaction_id)
8689

87-
# Tell seer to delete grouping records for these groups
88-
may_schedule_task_to_delete_hashes_from_seer(error_ids)
89-
9090
# Removing GroupHash rows prevents new events from associating to the groups
9191
# we just deleted.
9292
GroupHash.objects.filter(project_id=project.id, group__id__in=group_ids).delete()

src/sentry/issues/endpoints/organization_group_index.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import logging
23
from collections.abc import Mapping, Sequence
34
from datetime import datetime, timedelta
45
from typing import Any
@@ -67,6 +68,8 @@
6768
ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', '14d' and 'auto'"
6869
allowed_inbox_search_terms = frozenset(["date", "status", "for_review", "assigned_or_suggested"])
6970

71+
logger = logging.getLogger(__name__)
72+
7073

7174
def inbox_search(
7275
projects: Sequence[Project],
@@ -501,4 +504,8 @@ def delete(self, request: Request, organization: Organization) -> Response:
501504
self.get_environments(request, organization),
502505
)
503506

504-
return delete_groups(request, projects, organization.id, search_fn)
507+
try:
508+
return delete_groups(request, projects, organization.id, search_fn)
509+
except Exception:
510+
logger.exception("Error deleting groups")
511+
return Response({"detail": "Error deleting groups"}, status=500)

src/sentry/issues/endpoints/project_group_index.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import logging
23

34
from rest_framework.request import Request
45
from rest_framework.response import Response
@@ -30,6 +31,7 @@
3031

3132
ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', and '14d'"
3233
ERR_HASHES_AND_OTHER_QUERY = "Cannot use 'hashes' with 'query'"
34+
logger = logging.getLogger(__name__)
3335

3436

3537
@region_silo_endpoint
@@ -299,4 +301,8 @@ def delete(self, request: Request, project: Project) -> Response:
299301
:auth: required
300302
"""
301303
search_fn = functools.partial(prep_search, request, project)
302-
return delete_groups(request, [project], project.organization_id, search_fn)
304+
try:
305+
return delete_groups(request, [project], project.organization_id, search_fn)
306+
except Exception:
307+
logger.exception("Error deleting groups")
308+
return Response({"detail": "Error deleting groups"}, status=500)

tests/sentry/issues/endpoints/test_organization_group_index.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from unittest.mock import MagicMock, Mock, call, patch
88
from uuid import uuid4
99

10-
from django.db import OperationalError
10+
from django.db.utils import OperationalError
1111
from django.urls import reverse
1212
from django.utils import timezone
1313

@@ -4455,3 +4455,26 @@ def test_bulk_delete_performance_issues(self) -> None:
44554455
assert response.status_code == 204
44564456

44574457
self.assert_deleted_groups(groups)
4458+
4459+
@patch("sentry.api.helpers.group_index.delete.may_schedule_task_to_delete_hashes_from_seer")
4460+
def test_do_not_mark_as_pending_deletion_if_seer_fails(self, mock_seer_delete: Mock):
4461+
"""
4462+
Test that the issue is not marked as pending deletion if the seer call fails.
4463+
"""
4464+
# When trying to gather the hashes, the query could be cancelled by the user
4465+
mock_seer_delete.side_effect = OperationalError(
4466+
"QueryCanceled('canceling statement due to user request\n')"
4467+
)
4468+
event = self.store_event(data={}, project_id=self.project.id)
4469+
group1 = Group.objects.get(id=event.group_id)
4470+
assert GroupHash.objects.filter(group=group1).exists()
4471+
4472+
self.login_as(user=self.user)
4473+
with self.tasks():
4474+
response = self.get_response(qs_params={"id": [group1.id]})
4475+
assert response.status_code == 500
4476+
assert response.data["detail"] == "Error deleting groups"
4477+
4478+
# The group has not been marked as pending deletion
4479+
assert Group.objects.get(id=group1.id).status == group1.status
4480+
assert GroupHash.objects.filter(group=group1).exists()

tests/snuba/api/endpoints/test_project_group_index.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from uuid import uuid4
1010

1111
from django.conf import settings
12+
from django.db.utils import OperationalError
1213
from django.utils import timezone
1314

1415
from sentry.integrations.models.external_issue import ExternalIssue
@@ -1637,26 +1638,28 @@ def test_bulk_delete_performance_issues(self):
16371638
self.assert_groups_are_gone(groups)
16381639

16391640
@patch("sentry.api.helpers.group_index.delete.may_schedule_task_to_delete_hashes_from_seer")
1640-
@patch("sentry.utils.audit.log_service.record_audit_log")
1641-
def test_audit_log_even_if_exception_raised(
1642-
self, mock_record_audit_log: Mock, mock_seer_delete: Mock
1643-
):
1641+
def test_do_not_mark_as_pending_deletion_if_seer_fails(self, mock_seer_delete: Mock):
16441642
"""
1645-
Test that audit log is created even if an exception is raised after the audit log is created.
1643+
Test that the issue is not marked as pending deletion if the seer call fails.
16461644
"""
1647-
# Calling seer happens after creating the audit log entry
1648-
mock_seer_delete.side_effect = Exception("Seer error!")
1649-
group1 = self.create_group()
1645+
# When trying to gather the hashes, the query could be cancelled by the user
1646+
mock_seer_delete.side_effect = OperationalError(
1647+
"QueryCanceled('canceling statement due to user request\n')"
1648+
)
1649+
event = self.store_event(data={}, project_id=self.project.id)
1650+
group1 = Group.objects.get(id=event.group_id)
1651+
assert GroupHash.objects.filter(group=group1).exists()
1652+
16501653
self.login_as(user=self.user)
16511654
url = f"{self.path}?id={group1.id}"
16521655
with self.tasks():
16531656
response = self.client.delete(url, format="json")
16541657
assert response.status_code == 500
1658+
assert response.data["detail"] == "Error deleting groups"
16551659

1656-
self.assert_audit_log_entry([group1], mock_record_audit_log)
1657-
1658-
# They have been marked as pending deletion but the exception prevented their complete deletion
1659-
assert Group.objects.get(id=group1.id).status == GroupStatus.PENDING_DELETION
1660+
# The group has not been marked as pending deletion
1661+
assert Group.objects.get(id=group1.id).status == group1.status
1662+
assert GroupHash.objects.filter(group=group1).exists()
16601663

16611664
def test_new_event_for_pending_deletion_group_creates_new_group(self):
16621665
"""Test that after deleting a group, new events with the same fingerprint create a new group."""

0 commit comments

Comments
 (0)