Skip to content

Commit 313b5d5

Browse files
mifu67getsantry[bot]
authored andcommitted
chore(aci milestone 3): backfill resolution action filters (#91402)
Backfill migrated metric alerts with resolution action filter data conditions. See #89832 for an explanation of what these are. --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent db3a1ad commit 313b5d5

File tree

3 files changed

+172
-1
lines changed

3 files changed

+172
-1
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ tempest: 0002_make_message_type_nullable
2929

3030
uptime: 0042_extra_uptime_indexes
3131

32-
workflow_engine: 0060_rename_azure_devops_action_to_vsts
32+
workflow_engine: 0061_backfill_metric_alert_resolution_action_filters
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Generated by Django 5.1.7 on 2025-05-08 21:27
2+
from enum import Enum
3+
4+
from django.apps.registry import Apps
5+
from django.db import migrations, router, transaction
6+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
7+
8+
from sentry.new_migrations.migrations import CheckedMigration
9+
from sentry.utils.query import RangeQuerySetWrapper
10+
11+
12+
class AlertRuleStatus(Enum):
13+
PENDING = 0
14+
SNAPSHOT = 4
15+
DISABLED = 5
16+
NOT_ENOUGH_DATA = 6
17+
18+
19+
def backfill_resolution_action_filters(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
20+
AlertRule = apps.get_model("sentry", "AlertRule")
21+
Organization = apps.get_model("sentry", "Organization")
22+
23+
AlertRuleWorkflow = apps.get_model("workflow_engine", "AlertRuleWorkflow")
24+
DataCondition = apps.get_model("workflow_engine", "DataCondition")
25+
DataConditionGroup = apps.get_model("workflow_engine", "DataConditionGroup")
26+
27+
for organization in RangeQuerySetWrapper(Organization.objects.all()):
28+
organization_id = organization.id
29+
alert_rule_ids = AlertRule.objects_with_snapshots.filter(
30+
organization_id=organization_id, status=AlertRuleStatus.PENDING.value
31+
).values_list("id", flat=True)
32+
33+
workflow_ids = AlertRuleWorkflow.objects.filter(
34+
alert_rule_id__in=alert_rule_ids
35+
).values_list("workflow__id", flat=True)
36+
37+
for workflow_id in workflow_ids:
38+
workflow_dcgs = DataConditionGroup.objects.filter(
39+
workflowdataconditiongroup__workflow__id=workflow_id
40+
)
41+
42+
with transaction.atomic(router.db_for_write(DataCondition)):
43+
for dcg in workflow_dcgs:
44+
if DataCondition.objects.filter(
45+
condition_group=dcg, type="issue_priority_deescalating"
46+
).exists():
47+
# the resolution action filter has already been created, either via dual write or a previous migration
48+
# I have this inside the loop because it's possible someone added a new condition after we turned
49+
# on dual write of resolution action filters, so one of the conditions is correct and the other
50+
# needs to be backfilled
51+
continue
52+
53+
action_filter = DataCondition.objects.get(
54+
condition_group=dcg,
55+
)
56+
comparison = action_filter.comparison
57+
DataCondition.objects.create(
58+
comparison=comparison,
59+
condition_result=True,
60+
type="issue_priority_deescalating",
61+
condition_group=dcg,
62+
)
63+
64+
65+
class Migration(CheckedMigration):
66+
# This flag is used to mark that a migration shouldn't be automatically run in production.
67+
# This should only be used for operations where it's safe to run the migration after your
68+
# code has deployed. So this should not be used for most operations that alter the schema
69+
# of a table.
70+
# Here are some things that make sense to mark as post deployment:
71+
# - Large data migrations. Typically we want these to be run manually so that they can be
72+
# monitored and not block the deploy for a long period of time while they run.
73+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
74+
# run this outside deployments so that we don't block them. Note that while adding an index
75+
# is a schema change, it's completely safe to run the operation after the code has deployed.
76+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
77+
78+
is_post_deployment = True
79+
80+
dependencies = [
81+
("workflow_engine", "0060_rename_azure_devops_action_to_vsts"),
82+
]
83+
84+
operations = [
85+
migrations.RunPython(
86+
backfill_resolution_action_filters,
87+
migrations.RunPython.noop,
88+
hints={"tables": ["workflow_engine_alertruleworkflow"]},
89+
),
90+
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
3+
from sentry.testutils.cases import TestMigrations
4+
from sentry.workflow_engine.models import Condition, DataCondition, DataConditionGroup
5+
from sentry.workflow_engine.types import DetectorPriorityLevel
6+
7+
8+
@pytest.mark.skip("Timeout failures—skipping these tests, which pass, to unblock migration.")
9+
class TestBackfillMetricAlertResolutionActionFilters(TestMigrations):
10+
app = "workflow_engine"
11+
migrate_from = "0060_rename_azure_devops_action_to_vsts"
12+
migrate_to = "0061_backfill_metric_alert_resolution_action_filters"
13+
14+
def mock_aci_objects(self) -> tuple[DataConditionGroup, DataConditionGroup]:
15+
alert_rule = self.create_alert_rule(organization=self.organization)
16+
workflow = self.create_workflow(organization=self.organization)
17+
self.create_alert_rule_workflow(alert_rule_id=alert_rule.id, workflow=workflow)
18+
19+
critical_dcg = self.create_data_condition_group(organization=self.organization)
20+
self.create_workflow_data_condition_group(workflow=workflow, condition_group=critical_dcg)
21+
self.create_data_condition(
22+
comparison=DetectorPriorityLevel.HIGH,
23+
condition_result=True,
24+
type=Condition.ISSUE_PRIORITY_GREATER_OR_EQUAL,
25+
condition_group=critical_dcg,
26+
)
27+
28+
warning_dcg = self.create_data_condition_group(organization=self.organization)
29+
self.create_workflow_data_condition_group(workflow=workflow, condition_group=warning_dcg)
30+
self.create_data_condition(
31+
comparison=DetectorPriorityLevel.MEDIUM,
32+
condition_result=True,
33+
type=Condition.ISSUE_PRIORITY_GREATER_OR_EQUAL,
34+
condition_group=warning_dcg,
35+
)
36+
37+
return critical_dcg, warning_dcg
38+
39+
def create_resolve_action_filter(
40+
self, dcg: DataConditionGroup, comparison: DetectorPriorityLevel
41+
) -> None:
42+
self.create_data_condition(
43+
comparison=comparison,
44+
condition_result=True,
45+
type=Condition.ISSUE_PRIORITY_DEESCALATING,
46+
condition_group=dcg,
47+
)
48+
49+
def assert_resolve_action_filter_exists(
50+
self, dcg: DataConditionGroup, comparison: DetectorPriorityLevel
51+
) -> None:
52+
queryset = DataCondition.objects.filter(
53+
comparison=comparison, type=Condition.ISSUE_PRIORITY_DEESCALATING, condition_group=dcg
54+
)
55+
assert queryset.exists()
56+
assert queryset.count() == 1
57+
58+
def setup_initial_state(self):
59+
# vanilla
60+
self.critical_dcg_1, self.warning_dcg_1 = self.mock_aci_objects()
61+
62+
# both dcgs have a resolution action filter
63+
self.critical_dcg_2, self.warning_dcg_2 = self.mock_aci_objects()
64+
self.create_resolve_action_filter(self.critical_dcg_2, DetectorPriorityLevel.HIGH)
65+
self.create_resolve_action_filter(self.warning_dcg_2, DetectorPriorityLevel.MEDIUM)
66+
67+
# only one dcg has a resolution action filter
68+
self.critical_dcg_3, self.warning_dcg_3 = self.mock_aci_objects()
69+
self.create_resolve_action_filter(self.warning_dcg_3, DetectorPriorityLevel.MEDIUM)
70+
71+
def test_simple(self):
72+
self.assert_resolve_action_filter_exists(self.critical_dcg_1, DetectorPriorityLevel.HIGH)
73+
self.assert_resolve_action_filter_exists(self.warning_dcg_1, DetectorPriorityLevel.MEDIUM)
74+
75+
def test_both_migrated(self):
76+
self.assert_resolve_action_filter_exists(self.critical_dcg_2, DetectorPriorityLevel.HIGH)
77+
self.assert_resolve_action_filter_exists(self.warning_dcg_2, DetectorPriorityLevel.MEDIUM)
78+
79+
def test_one_migrated(self):
80+
self.assert_resolve_action_filter_exists(self.critical_dcg_3, DetectorPriorityLevel.HIGH)
81+
self.assert_resolve_action_filter_exists(self.warning_dcg_3, DetectorPriorityLevel.MEDIUM)

0 commit comments

Comments
 (0)