diff --git a/src/sentry/deletions/defaults/detector.py b/src/sentry/deletions/defaults/detector.py index cc8e8503a368a8..305f117f53fa3b 100644 --- a/src/sentry/deletions/defaults/detector.py +++ b/src/sentry/deletions/defaults/detector.py @@ -10,10 +10,29 @@ class DetectorDeletionTask(ModelDeletionTask[Detector]): manager_name = "objects_for_deletion" def get_child_relations(self, instance: Detector) -> list[BaseRelation]: - from sentry.workflow_engine.models import DataConditionGroup, DataSource + from sentry.incidents.models.alert_rule import AlertRule + from sentry.workflow_engine.models import ( + AlertRuleDetector, + DataConditionGroup, + DataSource, + ) model_relations: list[BaseRelation] = [] + # If this detector was dual-written from an AlertRule, cascade deletion + # to the AlertRule. AlertRuleDeletionTask will handle cleaning up + # AlertRuleDetector, AlertRuleWorkflow, triggers, and incidents. + # NOTE: AlertRule must be deleted before DataSource so that + # QuerySubscriptionDeletionTask sees no remaining AlertRule referencing + # the SnubaQuery and can safely delete it. + alert_rule_ids = list( + AlertRuleDetector.objects.filter(detector_id=instance.id).values_list( + "alert_rule_id", flat=True + ) + ) + if alert_rule_ids: + model_relations.append(ModelRelation(AlertRule, {"id__in": alert_rule_ids})) + # check that no other rows are related to the data source data_source_ids = DataSource.objects.filter(detector=instance.id).values_list( "id", flat=True diff --git a/tests/sentry/deletions/test_detector.py b/tests/sentry/deletions/test_detector.py index ffa4c85ee84edb..84b4fe99a32ae5 100644 --- a/tests/sentry/deletions/test_detector.py +++ b/tests/sentry/deletions/test_detector.py @@ -5,10 +5,14 @@ from sentry.constants import ObjectStatus from sentry.deletions.tasks.scheduled import reattempt_deletions, run_scheduled_deletions from sentry.incidents.grouptype import MetricIssue +from sentry.incidents.models.alert_rule import AlertRule, AlertRuleTrigger from sentry.snuba.models import QuerySubscription, SnubaQuery from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.uptime.models import UptimeSubscription, get_uptime_subscription +from sentry.workflow_engine.migration_helpers.alert_rule import dual_write_alert_rule from sentry.workflow_engine.models import ( + AlertRuleDetector, + AlertRuleWorkflow, DataCondition, DataConditionGroup, DataSource, @@ -262,3 +266,34 @@ def test_delete_uptime_subscription_without_detector(self) -> None: run_scheduled_deletions() assert not UptimeSubscription.objects.filter(id=uptime_sub_id).exists() + + +class DeleteDualWrittenDetectorTest(BaseWorkflowTest, HybridCloudTestMixin): + def test_deleting_detector_deletes_associated_alert_rule(self) -> None: + alert_rule = self.create_alert_rule( + organization=self.organization, + projects=[self.project], + ) + trigger = self.create_alert_rule_trigger(alert_rule=alert_rule) + dual_write_alert_rule(alert_rule) + + snuba_query = alert_rule.snuba_query + subscription = QuerySubscription.objects.get(snuba_query=snuba_query) + detector = AlertRuleDetector.objects.get(alert_rule_id=alert_rule.id).detector + data_source = DataSource.objects.get(source_id=str(subscription.id)) + + detector.status = ObjectStatus.PENDING_DELETION + detector.save() + self.ScheduledDeletion.schedule(instance=detector, days=0) + + with self.tasks(): + run_scheduled_deletions() + + assert not Detector.objects.filter(id=detector.id).exists() + assert not AlertRuleDetector.objects.filter(alert_rule_id=alert_rule.id).exists() + assert not AlertRuleWorkflow.objects.filter(alert_rule_id=alert_rule.id).exists() + assert not AlertRule.objects.filter(id=alert_rule.id).exists() + assert not AlertRuleTrigger.objects.filter(id=trigger.id).exists() + assert not DataSource.objects.filter(id=data_source.id).exists() + assert not QuerySubscription.objects.filter(id=subscription.id).exists() + assert not SnubaQuery.objects.filter(id=snuba_query.id).exists() diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py b/tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py index e2de2217d55f94..dcaae8de2ab726 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py @@ -9,9 +9,11 @@ from sentry.deletions.tasks.scheduled import run_scheduled_deletions from sentry.grouping.grouptype import ErrorGroupType from sentry.incidents.grouptype import MetricIssue +from sentry.incidents.models.alert_rule import AlertRule from sentry.models.environment import Environment from sentry.monitors.grouptype import MonitorIncidentType from sentry.search.utils import _HACKY_INVALID_USER +from sentry.snuba.models import QuerySubscription, SnubaQuery from sentry.testutils.asserts import assert_org_audit_log_exists from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.datetime import before_now @@ -23,7 +25,12 @@ DATA_SOURCE_UPTIME_SUBSCRIPTION, ) from sentry.workflow_engine.endpoints.organization_detector_index import convert_assignee_values -from sentry.workflow_engine.models import DataConditionGroup, Detector +from sentry.workflow_engine.migration_helpers.alert_rule import dual_write_alert_rule +from sentry.workflow_engine.models import ( + AlertRuleDetector, + DataConditionGroup, + Detector, +) from sentry.workflow_engine.models.detector_group import DetectorGroup from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType @@ -1369,3 +1376,30 @@ def test_delete_system_and_user_created_with_query_filters(self) -> None: ) self.assert_unaffected_detectors([self.detector, error_detector]) + + def test_delete_dual_written_detector_cleans_up_alert_rule(self) -> None: + alert_rule = self.create_alert_rule( + organization=self.organization, + projects=[self.project], + ) + self.create_alert_rule_trigger(alert_rule=alert_rule) + dual_write_alert_rule(alert_rule) + + detector = AlertRuleDetector.objects.get(alert_rule_id=alert_rule.id).detector + snuba_query = alert_rule.snuba_query + subscription = QuerySubscription.objects.get(snuba_query=snuba_query) + + with outbox_runner(): + self.get_success_response( + self.organization.slug, + qs_params={"id": str(detector.id)}, + status_code=204, + ) + + with self.tasks(): + run_scheduled_deletions() + + assert not Detector.objects.filter(id=detector.id).exists() + assert not AlertRule.objects.filter(id=alert_rule.id).exists() + assert not QuerySubscription.objects.filter(id=subscription.id).exists() + assert not SnubaQuery.objects.filter(id=snuba_query.id).exists()