From 80f5aa7c1a09d0f2e6b08f0151ccac14ac0bad42 Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 12:48:44 -0800 Subject: [PATCH 1/5] feat(aci): Make Workflow.when_condition_group unique --- ...01_workflow_when_condition_group_unique.py | 41 +++++++++++++++++++ src/sentry/workflow_engine/models/workflow.py | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py diff --git a/src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py b/src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py new file mode 100644 index 00000000000000..2f901ec55f0780 --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.8 on 2025-11-20 20:38 + +import django.db.models.deletion +from django.db import migrations + +import sentry.db.models.fields.foreignkey +from sentry.new_migrations.migrations import CheckedMigration + + +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", "0100_move_is_single_written_to_pending"), + ] + + operations = [ + migrations.AlterField( + model_name="workflow", + name="when_condition_group", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workflow_engine.dataconditiongroup", + unique=True, + ), + ), + ] diff --git a/src/sentry/workflow_engine/models/workflow.py b/src/sentry/workflow_engine/models/workflow.py index 07cfc53202b60d..9512c215ad85e6 100644 --- a/src/sentry/workflow_engine/models/workflow.py +++ b/src/sentry/workflow_engine/models/workflow.py @@ -69,7 +69,7 @@ class Workflow(DefaultFieldsModel, OwnerModel, JSONConfigBase): # Required as the 'when' condition for the workflow, this evaluates states emitted from the detectors when_condition_group = FlexibleForeignKey( - "workflow_engine.DataConditionGroup", null=True, blank=True + "workflow_engine.DataConditionGroup", null=True, blank=True, unique=True ) environment = FlexibleForeignKey("sentry.Environment", null=True, blank=True) From 22d31ea27ba41cc770e614933b413f4536cc6ca8 Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 13:10:27 -0800 Subject: [PATCH 2/5] fix --- migrations_lockfile.txt | 2 +- ...unique.py => 0103_workflow_when_condition_group_unique.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/sentry/workflow_engine/migrations/{0101_workflow_when_condition_group_unique.py => 0103_workflow_when_condition_group_unique.py} (93%) diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index d78afee5c9ef22..436259ad9347f3 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -39,4 +39,4 @@ tempest: 0003_use_encrypted_char_field uptime: 0049_cleanup_failed_safe_deletes -workflow_engine: 0102_cleanup_failed_safe_deletes +workflow_engine: 0103_workflow_when_condition_group_unique diff --git a/src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py b/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py similarity index 93% rename from src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py rename to src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py index 2f901ec55f0780..890f735a30218a 100644 --- a/src/sentry/workflow_engine/migrations/0101_workflow_when_condition_group_unique.py +++ b/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.8 on 2025-11-20 20:38 +# Generated by Django 5.2.8 on 2025-11-20 21:06 import django.db.models.deletion from django.db import migrations @@ -23,7 +23,7 @@ class Migration(CheckedMigration): is_post_deployment = False dependencies = [ - ("workflow_engine", "0100_move_is_single_written_to_pending"), + ("workflow_engine", "0102_cleanup_failed_safe_deletes"), ] operations = [ From a71291bcaee4249ee32147093c99b1f049ea74b5 Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 13:12:02 -0800 Subject: [PATCH 3/5] post-deploy --- .../migrations/0103_workflow_when_condition_group_unique.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py b/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py index 890f735a30218a..a369021e568616 100644 --- a/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py +++ b/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py @@ -20,7 +20,7 @@ class Migration(CheckedMigration): # 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 + is_post_deployment = True dependencies = [ ("workflow_engine", "0102_cleanup_failed_safe_deletes"), From c987852b177de3302a86734603c1d0d12d857451 Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 14:09:09 -0800 Subject: [PATCH 4/5] switch to constraint --- migrations_lockfile.txt | 2 +- .../migrations/0103_add_unique_constraint.py | 76 +++++++++++++++++++ ...03_workflow_when_condition_group_unique.py | 41 ---------- src/sentry/workflow_engine/models/workflow.py | 8 +- 4 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 src/sentry/workflow_engine/migrations/0103_add_unique_constraint.py delete mode 100644 src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 436259ad9347f3..741de05aa2ea89 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -39,4 +39,4 @@ tempest: 0003_use_encrypted_char_field uptime: 0049_cleanup_failed_safe_deletes -workflow_engine: 0103_workflow_when_condition_group_unique +workflow_engine: 0103_add_unique_constraint diff --git a/src/sentry/workflow_engine/migrations/0103_add_unique_constraint.py b/src/sentry/workflow_engine/migrations/0103_add_unique_constraint.py new file mode 100644 index 00000000000000..f02bb1e572f99a --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0103_add_unique_constraint.py @@ -0,0 +1,76 @@ +# Generated by Django 5.2.8 on 2025-11-20 21:39 + +import django.db.models.deletion +from django.db import migrations, models + +import sentry.db.models.fields.foreignkey +from sentry.new_migrations.migrations import CheckedMigration + + +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 = True + + dependencies = [ + ("sentry", "1007_cleanup_failed_safe_deletes"), + ("workflow_engine", "0102_cleanup_failed_safe_deletes"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + # First add the unique constraint (creates a unique index) + migrations.AddConstraint( + model_name="workflow", + constraint=models.UniqueConstraint( + fields=("when_condition_group_id",), + name="workflow_engine_workflow_when_condition_group_id_11d9ba05_uniq", + ), + ), + # Then drop the old regular index (db_index=False) + migrations.AlterField( + model_name="workflow", + name="when_condition_group", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + blank=True, + db_index=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workflow_engine.dataconditiongroup", + ), + ), + ], + state_operations=[ + # Update Django's state to reflect the final model state + migrations.AlterField( + model_name="workflow", + name="when_condition_group", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + blank=True, + db_index=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workflow_engine.dataconditiongroup", + ), + ), + migrations.AddConstraint( + model_name="workflow", + constraint=models.UniqueConstraint( + fields=("when_condition_group_id",), + name="workflow_engine_workflow_when_condition_group_id_11d9ba05_uniq", + ), + ), + ], + ), + ] diff --git a/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py b/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py deleted file mode 100644 index a369021e568616..00000000000000 --- a/src/sentry/workflow_engine/migrations/0103_workflow_when_condition_group_unique.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 5.2.8 on 2025-11-20 21:06 - -import django.db.models.deletion -from django.db import migrations - -import sentry.db.models.fields.foreignkey -from sentry.new_migrations.migrations import CheckedMigration - - -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 = True - - dependencies = [ - ("workflow_engine", "0102_cleanup_failed_safe_deletes"), - ] - - operations = [ - migrations.AlterField( - model_name="workflow", - name="when_condition_group", - field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="workflow_engine.dataconditiongroup", - unique=True, - ), - ), - ] diff --git a/src/sentry/workflow_engine/models/workflow.py b/src/sentry/workflow_engine/models/workflow.py index 9512c215ad85e6..ee9bcc74bf6bfe 100644 --- a/src/sentry/workflow_engine/models/workflow.py +++ b/src/sentry/workflow_engine/models/workflow.py @@ -69,7 +69,7 @@ class Workflow(DefaultFieldsModel, OwnerModel, JSONConfigBase): # Required as the 'when' condition for the workflow, this evaluates states emitted from the detectors when_condition_group = FlexibleForeignKey( - "workflow_engine.DataConditionGroup", null=True, blank=True, unique=True + "workflow_engine.DataConditionGroup", null=True, blank=True, db_index=False ) environment = FlexibleForeignKey("sentry.Environment", null=True, blank=True) @@ -99,6 +99,12 @@ class Workflow(DefaultFieldsModel, OwnerModel, JSONConfigBase): class Meta: app_label = "workflow_engine" db_table = "workflow_engine_workflow" + constraints = [ + models.UniqueConstraint( + fields=["when_condition_group_id"], + name="workflow_engine_workflow_when_condition_group_id_11d9ba05_uniq", + ), + ] def get_audit_log_data(self) -> dict[str, Any]: return {"name": self.name} From c6be282a8d761eb9c8ab5ab42c98f47b3ec4e5ad Mon Sep 17 00:00:00 2001 From: Kyle Consalus Date: Thu, 20 Nov 2025 14:15:17 -0800 Subject: [PATCH 5/5] fix test --- tests/sentry/workflow_engine/processors/test_workflow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index fd5d721eac890c..603a32c3512c59 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -189,9 +189,9 @@ def test_same_environment_only(self) -> None: workflow=non_matching_env_workflow, ) - dcg = self.create_data_condition_group() + matching_dcg = self.create_data_condition_group() matching_env_workflow = self.create_workflow( - when_condition_group=dcg, + when_condition_group=matching_dcg, environment=env, ) self.create_detector_workflow( @@ -199,8 +199,9 @@ def test_same_environment_only(self) -> None: workflow=matching_env_workflow, ) + mismatched_dcg = self.create_data_condition_group() mismatched_env_workflow = self.create_workflow( - when_condition_group=dcg, environment=other_env + when_condition_group=mismatched_dcg, environment=other_env ) self.create_detector_workflow( detector=self.error_detector,