Skip to content

Commit 68e8db2

Browse files
swartzrockclaude
andcommitted
fix(deletions): Avoid DoesNotExist crash when FK target is already deleted
DetectorDeletionTask and WorkflowDeletionTask both accessed FK descriptors (instance.workflow_condition_group, instance.when_condition_group) to check whether to include a related DataConditionGroup in child relations. When the referenced row has already been deleted, Django raises DoesNotExist. Because _run_deletion swallows all exceptions in production without re-raising, this caused the deletion task to silently fail and enter an infinite retry loop via reattempt_deletions. Orgs with detectors or workflows in this state can stay stuck in DELETION_IN_PROGRESS indefinitely. Fix by reading the raw FK id field instead of going through the descriptor, which avoids the database lookup and the potential DoesNotExist entirely. Fixes SENTRY-5M8C Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4b290be commit 68e8db2

File tree

4 files changed

+34
-4
lines changed

4 files changed

+34
-4
lines changed

src/sentry/deletions/defaults/detector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ def get_child_relations(self, instance: Detector) -> list[BaseRelation]:
2626
):
2727
model_relations.append(ModelRelation(DataSource, {"detector": instance.id}))
2828

29-
if instance.workflow_condition_group:
29+
if instance.workflow_condition_group_id:
3030
model_relations.append(
31-
ModelRelation(DataConditionGroup, {"id": instance.workflow_condition_group.id})
31+
ModelRelation(DataConditionGroup, {"id": instance.workflow_condition_group_id})
3232
)
3333

3434
return model_relations

src/sentry/deletions/defaults/workflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ def get_child_relations(self, instance: Workflow) -> list[BaseRelation]:
2121

2222
model_relations.append(ModelRelation(DataConditionGroup, {"id__in": action_filter_ids}))
2323

24-
if instance.when_condition_group:
24+
if instance.when_condition_group_id:
2525
model_relations.append(
26-
ModelRelation(DataConditionGroup, {"id": instance.when_condition_group.id})
26+
ModelRelation(DataConditionGroup, {"id": instance.when_condition_group_id})
2727
)
2828

2929
return model_relations

tests/sentry/deletions/test_detector.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,20 @@ def test_delete_uptime_detector_succeeds_when_remove_seat_fails(
176176
# Verify the error path in DetectorDeletionTask was actually exercised.
177177
mock_remove_seat_subscriptions.assert_called_once()
178178

179+
def test_dangling_workflow_condition_group(self) -> None:
180+
"""Deletion succeeds when workflow_condition_group_id references a deleted DataConditionGroup."""
181+
# Simulate a dangling FK — points to a non-existent DataConditionGroup row
182+
Detector.objects_for_deletion.filter(id=self.detector.id).update(
183+
workflow_condition_group_id=999999
184+
)
185+
186+
self.ScheduledDeletion.schedule(instance=self.detector, days=0)
187+
188+
with self.tasks():
189+
run_scheduled_deletions()
190+
191+
assert not Detector.objects_for_deletion.filter(id=self.detector.id).exists()
192+
179193
def test_delete_uptime_subscription_without_detector(self) -> None:
180194
"""UptimeSubscription deletion proceeds when the detector no longer exists."""
181195
detector = self.create_uptime_detector()

tests/sentry/deletions/test_workflow.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ def setUp(self) -> None:
5959
self.workflow.status = ObjectStatus.PENDING_DELETION
6060
self.workflow.save()
6161

62+
def test_dangling_when_condition_group(self) -> None:
63+
"""Deletion succeeds when when_condition_group_id references a deleted DataConditionGroup."""
64+
from sentry.workflow_engine.models import Workflow
65+
66+
# Simulate a dangling FK — points to a non-existent DataConditionGroup row
67+
Workflow.objects_for_deletion.filter(id=self.workflow.id).update(
68+
when_condition_group_id=999999
69+
)
70+
71+
self.ScheduledDeletion.schedule(instance=self.workflow, days=0)
72+
73+
with self.tasks():
74+
run_scheduled_deletions()
75+
76+
assert not Workflow.objects_for_deletion.filter(id=self.workflow.id).exists()
77+
6278
@pytest.mark.parametrize(
6379
"instance_attr",
6480
[

0 commit comments

Comments
 (0)