diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index ca7d37792e64c8..af1ae4404540c4 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -39,4 +39,4 @@ tempest: 0003_use_encrypted_char_field uptime: 0048_delete_uptime_status_columns -workflow_engine: 0099_backfill_metric_issue_detectorgroup +workflow_engine: 0100_move_is_single_written_to_pending diff --git a/src/sentry/workflow_engine/migrations/0100_move_is_single_written_to_pending.py b/src/sentry/workflow_engine/migrations/0100_move_is_single_written_to_pending.py new file mode 100644 index 00000000000000..ee0a206c8b6fe8 --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0100_move_is_single_written_to_pending.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.8 on 2025-11-19 18:22 + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.new_migrations.monkey.fields import SafeRemoveField +from sentry.new_migrations.monkey.state import DeletionAction + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("workflow_engine", "0099_backfill_metric_issue_detectorgroup"), + ] + + operations = [ + SafeRemoveField( + model_name="workflowfirehistory", + name="is_single_written", + deletion_action=DeletionAction.MOVE_TO_PENDING, + ), + ] diff --git a/src/sentry/workflow_engine/models/workflow_fire_history.py b/src/sentry/workflow_engine/models/workflow_fire_history.py index 28690f00a88336..e1a6a9ec8088af 100644 --- a/src/sentry/workflow_engine/models/workflow_fire_history.py +++ b/src/sentry/workflow_engine/models/workflow_fire_history.py @@ -16,7 +16,6 @@ class WorkflowFireHistory(DefaultFieldsModel): group = FlexibleForeignKey("sentry.Group", db_constraint=False) event_id = CharField(max_length=32) notification_uuid = UUIDField(auto_add=True, unique=True) - is_single_written = models.BooleanField(db_default=False, db_index=True) class Meta: db_table = "workflow_engine_workflowfirehistory" diff --git a/src/sentry/workflow_engine/processors/workflow_fire_history.py b/src/sentry/workflow_engine/processors/workflow_fire_history.py index 8ce8a92d697867..a5718c4bf0f1a8 100644 --- a/src/sentry/workflow_engine/processors/workflow_fire_history.py +++ b/src/sentry/workflow_engine/processors/workflow_fire_history.py @@ -70,7 +70,6 @@ def create_workflow_fire_histories( workflow_id=workflow_id, group=event_data.group, event_id=event_id, - is_single_written=True, ) for workflow_id in workflow_ids ] diff --git a/tests/sentry/api/serializers/test_rule.py b/tests/sentry/api/serializers/test_rule.py index 8a635c3fefebd7..e142d1c7b1d095 100644 --- a/tests/sentry/api/serializers/test_rule.py +++ b/tests/sentry/api/serializers/test_rule.py @@ -44,7 +44,7 @@ def test_last_triggered_with_workflow_only(self) -> None: workflow = IssueAlertMigrator(rule).run() WorkflowFireHistory.objects.create( - workflow=workflow, group=self.group, event_id="test-event-id", is_single_written=True + workflow=workflow, group=self.group, event_id="test-event-id" ) result = serialize(rule, self.user, RuleSerializer(expand=["lastTriggered"])) @@ -63,27 +63,12 @@ def test_last_triggered_with_workflow(self) -> None: # Create a newer WorkflowFireHistory WorkflowFireHistory.objects.create( - workflow=workflow, group=self.group, event_id="test-event-id", is_single_written=True + workflow=workflow, group=self.group, event_id="test-event-id" ) result = serialize(rule, self.user, RuleSerializer(expand=["lastTriggered"])) assert result["lastTriggered"] == timezone.now() - def test_last_triggered_workflow_ignore_single_written_false(self) -> None: - """Test that WorkflowFireHistory with is_single_written=False is ignored.""" - rule = self.create_project_rule() - - # Create a workflow for the rule - workflow = IssueAlertMigrator(rule).run() - - # Create a WorkflowFireHistory with is_single_written=False - WorkflowFireHistory.objects.create( - workflow=workflow, group=self.group, event_id="test-event-id", is_single_written=False - ) - - result = serialize(rule, self.user, RuleSerializer(expand=["lastTriggered"])) - assert result["lastTriggered"] is None - @freeze_time() class WorkflowRuleSerializerTest(TestCase): diff --git a/tests/sentry/workflow_engine/endpoints/serializers/test_timeseries_value_serializer.py b/tests/sentry/workflow_engine/endpoints/serializers/test_timeseries_value_serializer.py index 662d2c951bc6e6..1797580ca90854 100644 --- a/tests/sentry/workflow_engine/endpoints/serializers/test_timeseries_value_serializer.py +++ b/tests/sentry/workflow_engine/endpoints/serializers/test_timeseries_value_serializer.py @@ -44,7 +44,6 @@ def setUp(self) -> None: WorkflowFireHistory( workflow=self.workflow, group=self.group, - is_single_written=True, ) ) @@ -54,17 +53,9 @@ def setUp(self) -> None: WorkflowFireHistory( workflow=self.workflow_2, group=self.group, - is_single_written=True, ) ) - # dual written WFH is ignored - WorkflowFireHistory.objects.create( - workflow=self.workflow_2, - group=self.group, - is_single_written=False, - ) - histories: list[WorkflowFireHistory] = WorkflowFireHistory.objects.bulk_create(self.history) # manually update date_added diff --git a/tests/sentry/workflow_engine/endpoints/serializers/test_workflow_group_history_serializer.py b/tests/sentry/workflow_engine/endpoints/serializers/test_workflow_group_history_serializer.py index 4e1beb00d41601..47cca155551bf5 100644 --- a/tests/sentry/workflow_engine/endpoints/serializers/test_workflow_group_history_serializer.py +++ b/tests/sentry/workflow_engine/endpoints/serializers/test_workflow_group_history_serializer.py @@ -39,7 +39,6 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group, event_id=uuid4().hex, - is_single_written=True, ) ) self.group_2 = self.create_group() @@ -53,7 +52,6 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group_2, event_id=uuid4().hex, - is_single_written=True, ) ) self.group_3 = self.create_group() @@ -68,17 +66,8 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group_3, event_id=uuid4().hex, - is_single_written=True, ) ) - # dual written WFH is ignored - WorkflowFireHistory.objects.create( - detector=self.detector_3, - workflow=self.workflow, - group=self.group_3, - event_id=uuid4().hex, - is_single_written=False, - ) # this will be ordered after the WFH with self.detector_1 self.detector_4 = self.create_detector( project_id=self.project.id, @@ -91,7 +80,6 @@ def setUp(self) -> None: workflow=self.workflow_2, group=self.group, event_id=uuid4().hex, - is_single_written=True, ) ) diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_group_history.py b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_group_history.py index b706dc7dc39707..87c1a61e869852 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_group_history.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_group_history.py @@ -32,7 +32,6 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group, event_id=uuid4().hex, - is_single_written=True, ) ) self.group_2 = self.create_group() @@ -41,7 +40,6 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group_2, event_id=uuid4().hex, - is_single_written=True, ) ) histories: list[WorkflowFireHistory] = WorkflowFireHistory.objects.bulk_create(self.history) diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_stats.py b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_stats.py index 27b3f77631f44f..9a8faea3e403e6 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_stats.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_stats.py @@ -28,7 +28,6 @@ def setUp(self) -> None: workflow=self.workflow, group=self.group, date_added=before_now(hours=i + 1), - is_single_written=True, ) ) @@ -38,7 +37,6 @@ def setUp(self) -> None: workflow=self.workflow_2, group=self.group, date_added=before_now(hours=i + 1), - is_single_written=True, ) ) diff --git a/tests/sentry/workflow_engine/migrations/test_0096_delete_non_single_written_fire_history.py b/tests/sentry/workflow_engine/migrations/test_0096_delete_non_single_written_fire_history.py index ad9fd81c79d04d..e8f7094198c729 100644 --- a/tests/sentry/workflow_engine/migrations/test_0096_delete_non_single_written_fire_history.py +++ b/tests/sentry/workflow_engine/migrations/test_0096_delete_non_single_written_fire_history.py @@ -1,7 +1,6 @@ import pytest from sentry.testutils.cases import TestMigrations -from sentry.workflow_engine.models import Detector, Workflow, WorkflowFireHistory @pytest.mark.skip @@ -15,6 +14,11 @@ def setup_initial_state(self) -> None: self.project = self.create_project(organization=self.org) self.group = self.create_group(project=self.project) + # Use historical model state from the apps registry + Detector = self.apps.get_model("workflow_engine", "Detector") + Workflow = self.apps.get_model("workflow_engine", "Workflow") + WorkflowFireHistory = self.apps.get_model("workflow_engine", "WorkflowFireHistory") + self.detector = Detector.objects.create( project=self.project, name="Test Detector", @@ -63,6 +67,9 @@ def setup_initial_state(self) -> None: ) def test_migration(self) -> None: + # Use the current model state after migration + from sentry.workflow_engine.models import WorkflowFireHistory + # Verify that non-single-written records are deleted assert not WorkflowFireHistory.objects.filter(id=self.fire_history_to_delete_1.id).exists() assert not WorkflowFireHistory.objects.filter(id=self.fire_history_to_delete_2.id).exists() @@ -71,7 +78,6 @@ def test_migration(self) -> None: assert WorkflowFireHistory.objects.filter(id=self.fire_history_to_keep_1.id).exists() assert WorkflowFireHistory.objects.filter(id=self.fire_history_to_keep_2.id).exists() - # Verify only single-written records remain + # Verify only 2 records remain (the ones that had is_single_written=True) remaining_records = WorkflowFireHistory.objects.all() assert remaining_records.count() == 2 - assert all(record.is_single_written for record in remaining_records)