-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathscheduledeletion.py
166 lines (135 loc) · 5.83 KB
/
scheduledeletion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from __future__ import annotations
import logging
from datetime import timedelta
from typing import Type
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_only_model,
region_silo_only_model,
)
from sentry.services.hybrid_cloud.user import RpcUser
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.silo import SiloMode
delete_logger = logging.getLogger("sentry.deletions.api")
def default_guid():
return uuid4().hex
def default_date_schedule():
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, days=30, hours=0, data=None, actor=None):
model = type(instance)
silo_mode = SiloMode.get_current_mode()
if silo_mode not in model._meta.silo_limit.modes and silo_mode != SiloMode.MONOLITH:
# Pre-empt the fact that our silo protections wouldn't fire for mismatched model <-> silo deletion objects.
raise model._meta.silo_limit.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_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):
return apps.get_model(self.app_label, self.model_name)
def get_instance(self):
return self.get_model().objects.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_only_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_only_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