Skip to content

Commit

Permalink
chore(deletions) Move ScheduledDeletion models to deletions
Browse files Browse the repository at this point in the history
Move the `ScheduledDeletion` model classes into `sentry.deletions` so
that all the deletion code is co-located.

Refs #77479
  • Loading branch information
markstory committed Sep 23, 2024
1 parent 377927e commit 6b9ca7f
Show file tree
Hide file tree
Showing 37 changed files with 240 additions and 220 deletions.
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/api_application_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from sentry.api.base import Endpoint, control_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.serializers import serialize
from sentry.deletions.models.scheduleddeletion import ScheduledDeletion
from sentry.models.apiapplication import ApiApplication, ApiApplicationStatus
from sentry.models.scheduledeletion import ScheduledDeletion


class ApiApplicationSerializer(serializers.Serializer):
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
UPTIME_AUTODETECTION,
)
from sentry.datascrubbing import validate_pii_config_update, validate_pii_selectors
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.hybridcloud.rpc import IDEMPOTENCY_KEY_LENGTH
from sentry.integrations.utils.codecov import has_codecov_integration
from sentry.lang.native.utils import (
Expand All @@ -75,7 +76,6 @@
from sentry.models.avatars.organization_avatar import OrganizationAvatar
from sentry.models.options.organization_option import OrganizationOption
from sentry.models.organization import Organization, OrganizationStatus
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.organizations.services.organization import organization_service
from sentry.organizations.services.organization.model import (
RpcOrganization,
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from sentry.apidocs.parameters import GlobalParams
from sentry.constants import RESERVED_PROJECT_SLUGS, ObjectStatus
from sentry.datascrubbing import validate_pii_config_update, validate_pii_selectors
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.dynamic_sampling import get_supported_biases_ids, get_user_biases
from sentry.grouping.enhancer import Enhancements
from sentry.grouping.enhancer.exceptions import InvalidEnhancerConfig
Expand All @@ -47,7 +48,6 @@
from sentry.models.project import Project
from sentry.models.projectbookmark import ProjectBookmark
from sentry.models.projectredirect import ProjectRedirect
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.notifications.utils import has_alert_integration
from sentry.tasks.delete_seer_grouping_records import call_seer_delete_project_grouping_records

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_rule_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
from sentry.apidocs.examples.issue_alert_examples import IssueAlertExamples
from sentry.apidocs.parameters import GlobalParams, IssueAlertParams
from sentry.constants import ObjectStatus
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.integrations.jira.actions.create_ticket import JiraCreateTicketAction
from sentry.integrations.jira_server.actions.create_ticket import JiraServerCreateTicketAction
from sentry.integrations.slack.tasks.find_channel_id_for_rule import find_channel_id_for_rule
from sentry.integrations.slack.utils.rule_status import RedisRuleStatus
from sentry.mediators.project_rules.updater import Updater
from sentry.models.rule import NeglectedRule, RuleActivity, RuleActivityType
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.rules.actions import trigger_sentry_app_action_creators_for_issues
from sentry.rules.actions.utils import get_changed_data, get_updated_rule_data
from sentry.signals import alert_rule_edited
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/team_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
from sentry.apidocs.examples.team_examples import TeamExamples
from sentry.apidocs.parameters import GlobalParams, TeamParams
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.models.team import Team, TeamStatus


Expand Down
11 changes: 11 additions & 0 deletions src/sentry/deletions/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sentry.deletions.models.scheduleddeletion import (
RegionScheduledDeletion,
ScheduledDeletion,
get_regional_scheduled_deletion,
)

__all__ = (
"get_regional_scheduled_deletion",
"ScheduledDeletion",
"RegionScheduledDeletion",
)
180 changes: 180 additions & 0 deletions src/sentry/deletions/models/scheduleddeletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
from __future__ import annotations

import logging
from datetime import datetime, timedelta
from typing import Any, Self
from uuid import uuid4

from django.apps import apps
from django.db import models
from django.utils import timezone

from sentry.backup.scopes import RelocationScope
from sentry.db.models import (
BoundedBigIntegerField,
JSONField,
Model,
control_silo_model,
region_silo_model,
)
from sentry.silo.base import SiloLimit, SiloMode
from sentry.users.services.user import RpcUser
from sentry.users.services.user.service import user_service

delete_logger = logging.getLogger("sentry.deletions.api")


def default_guid() -> str:
return uuid4().hex


def default_date_schedule() -> datetime:
return timezone.now() + timedelta(days=30)


class BaseScheduledDeletion(Model):
"""
ScheduledDeletions are, well, relations to arbitrary records in a particular silo that are due for deletion by
the tasks/deletion/scheduled.py job in the future. They are cancellable, and provide automatic, batched cascade
in an async way for performance reasons.
Note that BOTH region AND control silos need to be able to schedule deletions of different records that will be
reconciled in different places. For that reason, the ScheduledDeletion model is split into two identical models
representing this split. Use the corresponding ScheduledDeletion based on the silo of the model being scheduled
for deletion.
"""

class Meta:
abstract = True

__relocation_scope__ = RelocationScope.Excluded

guid = models.CharField(max_length=32, unique=True, default=default_guid)
app_label = models.CharField(max_length=64)
model_name = models.CharField(max_length=64)
object_id = BoundedBigIntegerField()
date_added = models.DateTimeField(default=timezone.now)
date_scheduled = models.DateTimeField(default=default_date_schedule)
actor_id = BoundedBigIntegerField(null=True)
data = JSONField(default={})
in_progress = models.BooleanField(default=False)

@classmethod
def schedule(
cls, instance: Model, days: int = 30, hours: int = 0, data: Any = None, actor: Any = None
) -> Self:
model = type(instance)
silo_mode = SiloMode.get_current_mode()
model_silo = getattr(model._meta, "silo_limit", None)
assert (
model_silo
), "model._meta.silo_limit undefined. This model cannot be used with deletions"
if silo_mode not in model_silo.modes and silo_mode != SiloMode.MONOLITH:
# Pre-empt the fact that our silo protections wouldn't fire for mismatched model <-> silo deletion objects.
raise SiloLimit.AvailabilityError(
f"{model!r} was scheduled for deletion by {cls!r}, but is unavailable in {silo_mode!r}"
)

model_name = model.__name__
record, created = cls.objects.create_or_update(
app_label=instance._meta.app_label,
model_name=model_name,
object_id=instance.pk,
values={
"date_scheduled": timezone.now() + timedelta(days=days, hours=hours),
"data": data or {},
"actor_id": actor.id if actor else None,
},
)
if not created:
record = cls.objects.get(
app_label=instance._meta.app_label,
model_name=model_name,
object_id=instance.pk,
)

delete_logger.info(
"object.delete.queued",
extra={
"object_id": instance.id,
"transaction_id": record.guid,
"model": type(instance).__name__,
},
)
return record

@classmethod
def cancel(cls, instance: Model):
model_name = type(instance).__name__
try:
deletion = cls.objects.get(
model_name=model_name, object_id=instance.pk, in_progress=False
)
except cls.DoesNotExist:
delete_logger.info(
"object.delete.canceled.failed",
extra={"object_id": instance.pk, "model": model_name},
)
return

deletion.delete()
delete_logger.info(
"object.delete.canceled",
extra={"object_id": instance.pk, "model": model_name},
)

def get_model(self) -> type[Any]:
return apps.get_model(self.app_label, self.model_name)

def get_instance(self) -> Model:
from sentry import deletions
from sentry.deletions.base import ModelDeletionTask

model = self.get_model()
deletion_task = deletions.get(model=model, query=None)
query_manager = model.objects
if isinstance(deletion_task, ModelDeletionTask):
query_manager = getattr(model, deletion_task.manager_name)
return query_manager.get(pk=self.object_id)

def get_actor(self) -> RpcUser | None:
if not self.actor_id:
return None

return user_service.get_user(user_id=self.actor_id)


@control_silo_model
class ScheduledDeletion(BaseScheduledDeletion):
"""
This model schedules deletions to be processed in control and monolith silo modes. All historic schedule deletions
occur in this table. In the future, when RegionScheduledDeletions have proliferated for the appropriate models,
we will allow any region models scheduled in this table to finish processing before ensuring that all models discretely
process in either this table or the region table.
"""

class Meta:
unique_together = (("app_label", "model_name", "object_id"),)
app_label = "sentry"
db_table = "sentry_scheduleddeletion"


@region_silo_model
class RegionScheduledDeletion(BaseScheduledDeletion):
"""
This model schedules deletions to be processed in region and monolith silo modes. As new region silo test coverage
increases, new scheduled deletions will begin to occur in this table. Monolith (current saas) will continue
processing them alongside the original scheduleddeletions table, but in the future this table will only be
processed by region silos.
"""

class Meta:
unique_together = (("app_label", "model_name", "object_id"),)
app_label = "sentry"
db_table = "sentry_regionscheduleddeletion"


def get_regional_scheduled_deletion(mode: SiloMode) -> type[BaseScheduledDeletion]:
if mode != SiloMode.CONTROL:
return RegionScheduledDeletion
return ScheduledDeletion
4 changes: 2 additions & 2 deletions src/sentry/deletions/tasks/scheduled.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from django.db import router, transaction
from django.utils import timezone

from sentry.exceptions import DeleteAborted
from sentry.models.scheduledeletion import (
from sentry.deletions.models.scheduleddeletion import (
BaseScheduledDeletion,
RegionScheduledDeletion,
ScheduledDeletion,
)
from sentry.exceptions import DeleteAborted
from sentry.signals import pending_delete
from sentry.silo.base import SiloMode
from sentry.tasks.base import instrumented_task, retry
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/incidents/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sentry.auth.access import SystemAccess
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS, ObjectStatus
from sentry.db.models import Model
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.incidents import tasks
from sentry.incidents.models.alert_rule import (
AlertRule,
Expand Down Expand Up @@ -62,7 +63,6 @@
from sentry.models.notificationaction import ActionService, ActionTarget
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.relay.config.metric_extraction import on_demand_metrics_feature_flags
from sentry.search.events.builder.base import BaseQueryBuilder
from sentry.search.events.constants import (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
from sentry.api.base import control_silo_endpoint
from sentry.api.serializers import serialize
from sentry.constants import ObjectStatus
from sentry.deletions.models.scheduleddeletion import ScheduledDeletion
from sentry.integrations.api.bases.organization_integrations import (
OrganizationIntegrationBaseEndpoint,
)
from sentry.integrations.api.serializers.models.integration import OrganizationIntegrationSerializer
from sentry.integrations.models.organization_integration import OrganizationIntegration
from sentry.models.scheduledeletion import ScheduledDeletion
from sentry.organizations.services.organization import RpcUserOrganizationContext
from sentry.shared_integrations.exceptions import ApiError, IntegrationError
from sentry.utils.audit import create_audit_entry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
from sentry.api.fields.empty_integer import EmptyIntegerField
from sentry.api.serializers import serialize
from sentry.constants import ObjectStatus
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.hybridcloud.rpc import coerce_id_from
from sentry.integrations.services.integration import integration_service
from sentry.models.commit import Commit
from sentry.models.repository import Repository
from sentry.models.scheduledeletion import RegionScheduledDeletion
from sentry.tasks.repository import repository_cascade_delete_on_hide


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
import sentry.db.models.fields.text
import sentry.db.models.fields.uuid
import sentry.db.models.indexes
import sentry.deletions.models.scheduleddeletion
import sentry.models.apiapplication
import sentry.models.apigrant
import sentry.models.apitoken
import sentry.models.broadcast
import sentry.models.groupshare
import sentry.models.scheduledeletion
import sentry.sentry_apps.models.sentry_app
import sentry.sentry_apps.models.sentry_app_installation
import sentry.sentry_apps.models.servicehook
Expand Down Expand Up @@ -2162,7 +2162,7 @@ class Migration(CheckedMigration):
(
"guid",
models.CharField(
default=sentry.models.scheduledeletion.default_guid,
default=sentry.deletions.models.scheduleddeletion.default_guid,
max_length=32,
unique=True,
),
Expand All @@ -2174,7 +2174,7 @@ class Migration(CheckedMigration):
(
"date_scheduled",
models.DateTimeField(
default=sentry.models.scheduledeletion.default_date_schedule
default=sentry.deletions.models.scheduleddeletion.default_date_schedule
),
),
("actor_id", sentry.db.models.fields.bounded.BoundedBigIntegerField(null=True)),
Expand Down Expand Up @@ -8993,7 +8993,7 @@ class Migration(CheckedMigration):
(
"guid",
models.CharField(
default=sentry.models.scheduledeletion.default_guid,
default=sentry.deletions.models.scheduleddeletion.default_guid,
max_length=32,
unique=True,
),
Expand All @@ -9005,7 +9005,7 @@ class Migration(CheckedMigration):
(
"date_scheduled",
models.DateTimeField(
default=sentry.models.scheduledeletion.default_date_schedule
default=sentry.deletions.models.scheduleddeletion.default_date_schedule
),
),
("actor_id", sentry.db.models.fields.bounded.BoundedBigIntegerField(null=True)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar

if TYPE_CHECKING:
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
from sentry.models.rule import Rule
from sentry.models.scheduledeletion import RegionScheduledDeletion


class ObjectStatus:
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/migrations/0515_slugify_invalid_monitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar

if TYPE_CHECKING:
from sentry.deletions.models.scheduleddeletion import BaseScheduledDeletion
from sentry.models.rule import Rule
from sentry.models.scheduledeletion import BaseScheduledDeletion
from sentry.monitors.models import Monitor


Expand Down
Loading

0 comments on commit 6b9ca7f

Please sign in to comment.