Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/sentry/db/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
"""
Expand Down
23 changes: 23 additions & 0 deletions src/sentry/new_migrations/monkey/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)

Expand Down
59 changes: 59 additions & 0 deletions tests/sentry/new_migrations/monkey/test_models.py
Original file line number Diff line number Diff line change
@@ -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)
Loading