diff --git a/src/sentry/db/router.py b/src/sentry/db/router.py index d86bfab3b93230..29f8d1f16aff59 100644 --- a/src/sentry/db/router.py +++ b/src/sentry/db/router.py @@ -68,8 +68,13 @@ class SiloRouter: historical_silo_assignments = { "authidentity_duplicate": SiloMode.CONTROL, "authprovider_duplicate": SiloMode.CONTROL, + "feedback_feedback": SiloMode.REGION, + "releases_commit": SiloMode.REGION, + "releases_commitfilechange": SiloMode.REGION, "sentry_actor": SiloMode.REGION, "sentry_alertruleactivations": SiloMode.REGION, + "sentry_dashboardwidgetsnapshot": SiloMode.REGION, + "sentry_datasecrecywaiver": SiloMode.REGION, "sentry_incidentseen": SiloMode.REGION, "sentry_incidentsubscription": SiloMode.REGION, "sentry_monitorlocation": SiloMode.REGION, @@ -78,6 +83,8 @@ class SiloRouter: "sentry_projectavatar": SiloMode.REGION, "sentry_scheduledjob": SiloMode.CONTROL, "sentry_teamavatar": SiloMode.REGION, + "uptime_projectuptimesubscription": SiloMode.REGION, + "workflow_engine_actiongroupstatus": SiloMode.REGION, "workflow_engine_workflowaction": SiloMode.REGION, } """ diff --git a/src/sentry/new_migrations/monkey/models.py b/src/sentry/new_migrations/monkey/models.py index e744e11b356e70..c3548ea438cf4e 100644 --- a/src/sentry/new_migrations/monkey/models.py +++ b/src/sentry/new_migrations/monkey/models.py @@ -1,8 +1,10 @@ +from django.db import router from django.db.migrations import DeleteModel from django_zero_downtime_migrations.backends.postgres.schema import UnsafeOperationException from sentry.db.postgres.schema import SafePostgresDatabaseSchemaEditor from sentry.new_migrations.monkey.state import DeletionAction, SentryProjectState +from sentry.utils.env import in_test_environment class SafeDeleteModel(DeleteModel): @@ -35,6 +37,27 @@ def database_forwards( return model = from_state.get_pending_deletion_model(app_label, self.name) + table = model._meta.db_table + + # Check if we can determine the model's database to detect missing + # historical_silo_assignments entries + resolved_db = None + for db_router in router.routers: + if hasattr(db_router, "_db_for_table"): + resolved_db = db_router._db_for_table(table, app_label) + if resolved_db is not None: + break + + # If we can't determine the database and we're in CI/tests, fail loudly + # This indicates the table is missing from historical_silo_assignments + if resolved_db is None and in_test_environment(): + raise ValueError( + f"Cannot determine database for deleted model {app_label}.{self.name} " + f"(table: {table}). This table must be added to historical_silo_assignments " + f"in src/sentry/db/router.py (or getsentry/db/router.py for getsentry models) " + f"with the appropriate SiloMode before the deletion migration can run. " + ) + if self.allow_migrate_model(schema_editor.connection.alias, model): schema_editor.delete_model(model, is_safe=True) diff --git a/tests/sentry/new_migrations/monkey/test_models.py b/tests/sentry/new_migrations/monkey/test_models.py new file mode 100644 index 00000000000000..583c4e5fd322b2 --- /dev/null +++ b/tests/sentry/new_migrations/monkey/test_models.py @@ -0,0 +1,59 @@ +""" +Tests for SafeDeleteModel validation that ensures deleted models are added to +historical_silo_assignments. +""" + +from typing import cast +from unittest.mock import Mock + +import pytest +from django.db import connection + +from sentry.db.postgres.schema import SafePostgresDatabaseSchemaEditor +from sentry.new_migrations.monkey.models import SafeDeleteModel +from sentry.new_migrations.monkey.state import DeletionAction, SentryProjectState +from sentry.testutils.cases import TestCase + + +class SafeDeleteModelTest(TestCase): + """ + Tests that SafeDeleteModel fails loudly when a deleted model is not in + historical_silo_assignments. + """ + + def test_delete_model_without_historical_assignment_fails(self) -> None: + """ + When deleting a model that is not in historical_silo_assignments and + cannot be found, SafeDeleteModel should raise a ValueError in test + environments. + """ + fake_meta = Mock() + fake_meta.db_table = "sentry_fake_deleted_table_not_in_router" + fake_meta.app_label = "sentry" + fake_meta.model_name = "fakedeletedmodel" + + FakeDeletedModel = Mock() + FakeDeletedModel._meta = fake_meta + + from_state = SentryProjectState() + to_state = SentryProjectState() + + # Manually add the model to pending deletion in the from_state + # This simulates what happens when a model was marked for pending deletion + # and is now being deleted + from_state.pending_deletion_models[("sentry", "fakedeletedmodel")] = FakeDeletedModel + + operation = SafeDeleteModel(name="FakeDeletedModel", deletion_action=DeletionAction.DELETE) + + with connection.schema_editor() as schema_editor: + with pytest.raises(ValueError) as exc_info: + operation.database_forwards( + "sentry", + cast(SafePostgresDatabaseSchemaEditor, schema_editor), + from_state, + to_state, + ) + + assert "Cannot determine database for deleted model" in str(exc_info.value) + assert "sentry_fake_deleted_table_not_in_router" in str(exc_info.value) + assert "historical_silo_assignments" in str(exc_info.value)