diff --git a/src/sentry/deletions/defaults/monitor.py b/src/sentry/deletions/defaults/monitor.py index dc7200bfd4da53..4de3fc87c67c70 100644 --- a/src/sentry/deletions/defaults/monitor.py +++ b/src/sentry/deletions/defaults/monitor.py @@ -1,3 +1,7 @@ +from collections.abc import Sequence + +from sentry import quotas +from sentry.constants import DataCategory from sentry.deletions.base import ( BaseRelation, BulkModelDeletionTask, @@ -17,3 +21,8 @@ def get_child_relations(self, instance: Monitor) -> list[BaseRelation]: ), ModelRelation(models.MonitorEnvironment, {"monitor_id": instance.id}), ] + + def delete_instance_bulk(self, instance_list: Sequence[Monitor]) -> None: + if instance_list: + quotas.backend.remove_seats(DataCategory.MONITOR_SEAT, instance_list) + super().delete_instance_bulk(instance_list) diff --git a/src/sentry/quotas/base.py b/src/sentry/quotas/base.py index d493eede4d4e3b..04a626c51801e8 100644 --- a/src/sentry/quotas/base.py +++ b/src/sentry/quotas/base.py @@ -648,6 +648,18 @@ def remove_seat(self, data_category: DataCategory, seat_object: SeatObject) -> N Removes an assigned seat. """ + def remove_seats(self, data_category: DataCategory, seat_objects: Sequence[SeatObject]) -> None: + """ + Removes assigned seats for a batch of objects. + + The default implementation simply iterates over ``seat_objects`` and calls + ``remove_seat`` for each item. Backends that need to perform additional bookkeeping + (for example, reducing database queries) can override this method to implement a more + efficient bulk removal. + """ + for seat_object in seat_objects: + self.remove_seat(data_category, seat_object) + def check_accept_monitor_checkin(self, project_id: int, monitor_slug: str): """ Will return a `PermitCheckInStatus`. diff --git a/tests/sentry/deletions/test_monitor.py b/tests/sentry/deletions/test_monitor.py index a77cee0753d81f..27561c8ffc136a 100644 --- a/tests/sentry/deletions/test_monitor.py +++ b/tests/sentry/deletions/test_monitor.py @@ -1,3 +1,6 @@ +from unittest import mock + +from sentry.constants import DataCategory from sentry.deletions.tasks.scheduled import run_scheduled_deletions from sentry.models.environment import Environment from sentry.models.project import Project @@ -46,3 +49,22 @@ def test_simple(self) -> None: # Shared objects should continue to exist. assert Environment.objects.filter(id=env.id).exists() assert Project.objects.filter(id=project.id).exists() + + @mock.patch("sentry.deletions.defaults.monitor.quotas.backend.remove_seats") + def test_removes_monitor_seats_before_delete(self, mock_remove_seats: mock.MagicMock) -> None: + project = self.create_project(name="with-seats") + monitor = Monitor.objects.create( + organization_id=project.organization.id, + project_id=project.id, + config={"schedule": "* * * * *", "schedule_type": ScheduleType.CRONTAB}, + ) + + self.ScheduledDeletion.schedule(instance=monitor, days=0) + + with self.tasks(): + run_scheduled_deletions() + + mock_remove_seats.assert_called_once() + args, _ = mock_remove_seats.call_args + assert args[0] == DataCategory.MONITOR_SEAT + assert list(args[1]) == [monitor]