Skip to content

Commit 90a15ea

Browse files
committed
fix(detectors): Ensure metric Detector deletion cleans up the AlertRule
1 parent 0db1d65 commit 90a15ea

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

src/sentry/deletions/defaults/detector.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,29 @@ class DetectorDeletionTask(ModelDeletionTask[Detector]):
1010
manager_name = "objects_for_deletion"
1111

1212
def get_child_relations(self, instance: Detector) -> list[BaseRelation]:
13-
from sentry.workflow_engine.models import DataConditionGroup, DataSource
13+
from sentry.incidents.models.alert_rule import AlertRule
14+
from sentry.workflow_engine.models import (
15+
AlertRuleDetector,
16+
DataConditionGroup,
17+
DataSource,
18+
)
1419

1520
model_relations: list[BaseRelation] = []
1621

22+
# If this detector was dual-written from an AlertRule, cascade deletion
23+
# to the AlertRule. AlertRuleDeletionTask will handle cleaning up
24+
# AlertRuleDetector, AlertRuleWorkflow, triggers, and incidents.
25+
# NOTE: AlertRule must be deleted before DataSource so that
26+
# QuerySubscriptionDeletionTask sees no remaining AlertRule referencing
27+
# the SnubaQuery and can safely delete it.
28+
alert_rule_ids = list(
29+
AlertRuleDetector.objects.filter(detector_id=instance.id).values_list(
30+
"alert_rule_id", flat=True
31+
)
32+
)
33+
if alert_rule_ids:
34+
model_relations.append(ModelRelation(AlertRule, {"id__in": alert_rule_ids}))
35+
1736
# check that no other rows are related to the data source
1837
data_source_ids = DataSource.objects.filter(detector=instance.id).values_list(
1938
"id", flat=True

tests/sentry/deletions/test_detector.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
from sentry.constants import ObjectStatus
66
from sentry.deletions.tasks.scheduled import run_scheduled_deletions
77
from sentry.incidents.grouptype import MetricIssue
8+
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleTrigger
89
from sentry.snuba.models import QuerySubscription, SnubaQuery
910
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
1011
from sentry.uptime.models import UptimeSubscription, get_uptime_subscription
12+
from sentry.workflow_engine.migration_helpers.alert_rule import dual_write_alert_rule
1113
from sentry.workflow_engine.models import (
14+
AlertRuleDetector,
15+
AlertRuleWorkflow,
1216
DataCondition,
1317
DataConditionGroup,
1418
DataSource,
@@ -196,3 +200,34 @@ def test_delete_uptime_subscription_without_detector(self) -> None:
196200
run_scheduled_deletions()
197201

198202
assert not UptimeSubscription.objects.filter(id=uptime_sub_id).exists()
203+
204+
205+
class DeleteDualWrittenDetectorTest(BaseWorkflowTest, HybridCloudTestMixin):
206+
def test_deleting_detector_deletes_associated_alert_rule(self) -> None:
207+
alert_rule = self.create_alert_rule(
208+
organization=self.organization,
209+
projects=[self.project],
210+
)
211+
trigger = self.create_alert_rule_trigger(alert_rule=alert_rule)
212+
dual_write_alert_rule(alert_rule)
213+
214+
snuba_query = alert_rule.snuba_query
215+
subscription = QuerySubscription.objects.get(snuba_query=snuba_query)
216+
detector = AlertRuleDetector.objects.get(alert_rule_id=alert_rule.id).detector
217+
data_source = DataSource.objects.get(source_id=str(subscription.id))
218+
219+
detector.status = ObjectStatus.PENDING_DELETION
220+
detector.save()
221+
self.ScheduledDeletion.schedule(instance=detector, days=0)
222+
223+
with self.tasks():
224+
run_scheduled_deletions()
225+
226+
assert not Detector.objects.filter(id=detector.id).exists()
227+
assert not AlertRuleDetector.objects.filter(alert_rule_id=alert_rule.id).exists()
228+
assert not AlertRuleWorkflow.objects.filter(alert_rule_id=alert_rule.id).exists()
229+
assert not AlertRule.objects.filter(id=alert_rule.id).exists()
230+
assert not AlertRuleTrigger.objects.filter(id=trigger.id).exists()
231+
assert not DataSource.objects.filter(id=data_source.id).exists()
232+
assert not QuerySubscription.objects.filter(id=subscription.id).exists()
233+
assert not SnubaQuery.objects.filter(id=snuba_query.id).exists()

tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sentry.deletions.tasks.scheduled import run_scheduled_deletions
1212
from sentry.grouping.grouptype import ErrorGroupType
1313
from sentry.incidents.grouptype import MetricIssue
14-
from sentry.incidents.models.alert_rule import AlertRuleDetectionType
14+
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleDetectionType
1515
from sentry.models.environment import Environment
1616
from sentry.monitors.grouptype import MonitorIncidentType
1717
from sentry.search.utils import _HACKY_INVALID_USER
@@ -37,7 +37,14 @@
3737
)
3838
from sentry.workflow_engine.endpoints.organization_detector_index import convert_assignee_values
3939
from sentry.workflow_engine.endpoints.validators.utils import get_unknown_detector_type_error
40-
from sentry.workflow_engine.models import DataCondition, DataConditionGroup, DataSource, Detector
40+
from sentry.workflow_engine.migration_helpers.alert_rule import dual_write_alert_rule
41+
from sentry.workflow_engine.models import (
42+
AlertRuleDetector,
43+
DataCondition,
44+
DataConditionGroup,
45+
DataSource,
46+
Detector,
47+
)
4148
from sentry.workflow_engine.models.data_condition import Condition
4249
from sentry.workflow_engine.models.detector_group import DetectorGroup
4350
from sentry.workflow_engine.models.detector_workflow import DetectorWorkflow
@@ -2007,3 +2014,30 @@ def test_delete_system_and_user_created_with_query_filters(self) -> None:
20072014
)
20082015

20092016
self.assert_unaffected_detectors([self.detector, error_detector])
2017+
2018+
def test_delete_dual_written_detector_cleans_up_alert_rule(self) -> None:
2019+
alert_rule = self.create_alert_rule(
2020+
organization=self.organization,
2021+
projects=[self.project],
2022+
)
2023+
self.create_alert_rule_trigger(alert_rule=alert_rule)
2024+
dual_write_alert_rule(alert_rule)
2025+
2026+
detector = AlertRuleDetector.objects.get(alert_rule_id=alert_rule.id).detector
2027+
snuba_query = alert_rule.snuba_query
2028+
subscription = QuerySubscription.objects.get(snuba_query=snuba_query)
2029+
2030+
with outbox_runner():
2031+
self.get_success_response(
2032+
self.organization.slug,
2033+
qs_params={"id": str(detector.id)},
2034+
status_code=204,
2035+
)
2036+
2037+
with self.tasks():
2038+
run_scheduled_deletions()
2039+
2040+
assert not Detector.objects.filter(id=detector.id).exists()
2041+
assert not AlertRule.objects.filter(id=alert_rule.id).exists()
2042+
assert not QuerySubscription.objects.filter(id=subscription.id).exists()
2043+
assert not SnubaQuery.objects.filter(id=snuba_query.id).exists()

0 commit comments

Comments
 (0)