Skip to content

Commit 39d1af2

Browse files
authored
feat(github-growth): manually cascade delete after hiding repo (#53018)
1 parent 71e1599 commit 39d1af2

File tree

5 files changed

+86
-25
lines changed

5 files changed

+86
-25
lines changed

src/sentry/api/endpoints/organization_repository_details.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sentry.constants import ObjectStatus
1313
from sentry.models import Commit, Integration, Repository, ScheduledDeletion
1414
from sentry.services.hybrid_cloud import coerce_id_from
15+
from sentry.tasks.repository import repository_cascade_delete_on_hide
1516

1617

1718
class RepositorySerializer(serializers.Serializer):
@@ -89,6 +90,8 @@ def put(self, request: Request, organization, repo_id) -> Response:
8990
):
9091
repo.reset_pending_deletion_field_names()
9192
repo.delete_pending_deletion_option()
93+
elif repo.status == ObjectStatus.HIDDEN and old_status != repo.status:
94+
repository_cascade_delete_on_hide.apply_async(kwargs={"repo_id": repo.id})
9295

9396
return Response(serialize(repo, request.user))
9497

src/sentry/deletions/base.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@
88
_leaf_re = re.compile(r"^(UserReport|Event|Group)(.+)")
99

1010

11+
def _delete_children(manager, relations, transaction_id=None, actor_id=None):
12+
# Ideally this runs through the deletion manager
13+
for relation in relations:
14+
task = manager.get(
15+
transaction_id=transaction_id,
16+
actor_id=actor_id,
17+
task=relation.task,
18+
**relation.params,
19+
)
20+
21+
# If we want smaller tasks then this also has to return when has_more is true.
22+
# This could significant increase the number of tasks we spawn. Get better estimates
23+
# by collecting metrics.
24+
has_more = True
25+
while has_more:
26+
has_more = task.chunk()
27+
if has_more:
28+
metrics.incr("deletions.should_spawn", tags={"task": type(task).__name__})
29+
return False
30+
31+
1132
class BaseRelation:
1233
def __init__(self, params, task):
1334
self.task = task
@@ -126,24 +147,7 @@ def delete_instance_bulk(self, instance_list):
126147
self.delete_instance(instance)
127148

128149
def delete_children(self, relations):
129-
# Ideally this runs through the deletion manager
130-
for relation in relations:
131-
task = self.manager.get(
132-
transaction_id=self.transaction_id,
133-
actor_id=self.actor_id,
134-
task=relation.task,
135-
**relation.params,
136-
)
137-
138-
# If we want smaller tasks then this also has to return when has_more is true.
139-
# This could significant increase the number of tasks we spawn. Get better estimates
140-
# by collecting metrics.
141-
has_more = True
142-
while has_more:
143-
has_more = task.chunk()
144-
if has_more:
145-
metrics.incr("deletions.should_spawn", tags={"task": type(task).__name__})
146-
return False
150+
return _delete_children(self.manager, relations, self.transaction_id, self.actor_id)
147151

148152
def mark_deletion_in_progress(self, instance_list):
149153
pass

src/sentry/deletions/defaults/repository.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
from ..base import ModelDeletionTask, ModelRelation
55

66

7+
def _get_repository_child_relations(instance):
8+
from sentry.models import Commit, PullRequest, RepositoryProjectPathConfig
9+
10+
return [
11+
ModelRelation(Commit, {"repository_id": instance.id}),
12+
ModelRelation(PullRequest, {"repository_id": instance.id}),
13+
ModelRelation(RepositoryProjectPathConfig, {"repository_id": instance.id}),
14+
]
15+
16+
717
class RepositoryDeletionTask(ModelDeletionTask):
818
def should_proceed(self, instance):
919
"""
@@ -12,13 +22,7 @@ def should_proceed(self, instance):
1222
return instance.status in {ObjectStatus.PENDING_DELETION, ObjectStatus.DELETION_IN_PROGRESS}
1323

1424
def get_child_relations(self, instance):
15-
from sentry.models import Commit, PullRequest, RepositoryProjectPathConfig
16-
17-
return [
18-
ModelRelation(Commit, {"repository_id": instance.id}),
19-
ModelRelation(PullRequest, {"repository_id": instance.id}),
20-
ModelRelation(RepositoryProjectPathConfig, {"repository_id": instance.id}),
21-
]
25+
return _get_repository_child_relations(instance)
2226

2327
def delete_instance(self, instance):
2428
# TODO child_relations should also send pending_delete so we

src/sentry/tasks/repository.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from sentry.deletions import default_manager
2+
from sentry.deletions.base import _delete_children
3+
from sentry.deletions.defaults.repository import _get_repository_child_relations
4+
from sentry.models.repository import Repository
5+
from sentry.tasks.base import instrumented_task
6+
7+
8+
@instrumented_task(name="sentry.models.repository_cascade_delete_on_hide", acks_late=True)
9+
def repository_cascade_delete_on_hide(repo_id: int) -> None:
10+
# Manually cause a deletion cascade.
11+
# This should be called after setting a repo's status
12+
# to ObjectStatus.HIDDEN.
13+
# References RepositoryDeletionTask and BaseDeletionTask logic.
14+
15+
try:
16+
repo = Repository.objects.get(id=repo_id)
17+
except Repository.DoesNotExist:
18+
return
19+
20+
has_more = True
21+
22+
while has_more:
23+
# get child relations
24+
child_relations = _get_repository_child_relations(repo)
25+
# extend relations
26+
child_relations = child_relations + [
27+
rel(repo) for rel in default_manager.dependencies[Repository]
28+
]
29+
# no need to filter relations; delete them
30+
if child_relations:
31+
has_more = _delete_children(manager=default_manager, relations=child_relations)

tests/sentry/api/endpoints/test_organization_repository_details.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,25 @@ def test_put_hide_repo(self):
267267
repo = Repository.objects.get(id=repo.id)
268268
assert repo.status == ObjectStatus.HIDDEN
269269

270+
def test_put_hide_repo_with_commits(self):
271+
self.login_as(user=self.user)
272+
273+
org = self.create_organization(owner=self.user, name="baz")
274+
repo = Repository.objects.create(
275+
name="example", organization_id=org.id, external_id="abc123"
276+
)
277+
Commit.objects.create(repository_id=repo.id, key="a" * 40, organization_id=org.id)
278+
279+
url = reverse("sentry-api-0-organization-repository-details", args=[org.slug, repo.id])
280+
281+
with self.tasks():
282+
response = self.client.put(url, data={"status": "hidden"})
283+
assert response.status_code == 200
284+
285+
repo = Repository.objects.get(id=repo.id)
286+
assert repo.status == ObjectStatus.HIDDEN
287+
assert len(Commit.objects.filter(repository_id=repo.id)) == 0
288+
270289
def test_put_cancel_deletion_duplicate_exists(self):
271290
self.login_as(user=self.user)
272291

0 commit comments

Comments
 (0)