Skip to content

Commit c7a8e2d

Browse files
authored
Projects: improve deletion (#12456)
For some related fields, Django first does a select, then it probably loads each object into memory? But after that it does the deletion using a `IN` statement with the ID of the objects... Using raw_delete avoids that, but it can also cause integrity problems, as Django does its own implementation of on_delete, so I chose models that don't have additional relations and also shouldn't have problems as they are always deleted when the project is deleted. Another source of problems is when models have foreign fields with on_delete=SET_NULL, as django will do an UPDATE over the model with all the related fields... for example for latest_build, django will run an update over all projects with an `IN (... all builds IDs..)`, to set the latest_build ID to null..., which is silly since the whole project model is being deleted, but django doesn't know that lates_build can only contain a build from the project. A raw delete could help, but builds and versions have a lot of related fields in other models, so I'm a little hesitant. Closes #12410 Ref #10040
1 parent a8a29fe commit c7a8e2d

File tree

3 files changed

+40
-4
lines changed

3 files changed

+40
-4
lines changed

readthedocs/projects/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,15 @@ def save(self, *args, **kwargs):
691691
def delete(self, *args, **kwargs):
692692
from readthedocs.projects.tasks.utils import clean_project_resources
693693

694+
# NOTE: We use _raw_delete to avoid Django fetching all objects
695+
# before the deletion. Be careful when using _raw_delete, signals
696+
# won't be sent, and can cause integrity problems if the model
697+
# has relations with other models.
698+
qs = self.page_views.all()
699+
qs._raw_delete(qs.db)
700+
qs = self.search_queries.all()
701+
qs._raw_delete(qs.db)
702+
694703
# Remove extra resources
695704
clean_project_resources(self)
696705

readthedocs/projects/tasks/utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ def clean_project_resources(project, version=None, version_slug=None):
8787
sometimes to clean old resources.
8888
8989
.. note::
90+
91+
This function shouldn't delete objects that can't be recreated
92+
by re-activating the version (e.g. page views, search queries),
93+
as it's used when a version is deactivated.
94+
95+
.. note::
96+
9097
This function is usually called just before deleting project.
9198
Make sure to not depend on the project object inside the tasks.
9299
"""
@@ -108,11 +115,16 @@ def clean_project_resources(project, version=None, version_slug=None):
108115
version_slug=version_slug,
109116
)
110117

111-
# Remove imported files
118+
# NOTE: We use _raw_delete to avoid Django fetching all objects
119+
# before the deletion. Be careful when using _raw_delete, signals
120+
# won't be sent, and can cause integrity problems if the model
121+
# has relations with other models.
112122
if version:
113-
version.imported_files.all().delete()
123+
qs = version.imported_files.all()
124+
qs._raw_delete(qs.db)
114125
else:
115-
project.imported_files.all().delete()
126+
qs = project.imported_files.all()
127+
qs._raw_delete(qs.db)
116128

117129

118130
@app.task()

readthedocs/projects/tests/test_models.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from django.test import TestCase
44
from django_dynamic_fixture import get
55

6-
from readthedocs.projects.models import Feature, Project
6+
from readthedocs.analytics.models import PageView
7+
from readthedocs.builds.models import Build, Version
8+
from readthedocs.projects.models import Feature, ImportedFile, Project
9+
from readthedocs.search.models import SearchQuery
710

811

912
class TestURLPatternsUtils(TestCase):
@@ -137,3 +140,15 @@ def test_proxied_api_prefix(self):
137140
self.assertEqual(self.project.proxied_api_url, "prefix/_/")
138141
self.assertEqual(self.project.proxied_api_host, "/prefix/_")
139142
self.assertEqual(self.project.proxied_api_prefix, "/prefix/")
143+
144+
def test_number_of_queries_on_project_deletion(self):
145+
for i in range(5):
146+
version = get(Version, project=self.project, slug=f"subproject-{i}", active=True, built=True)
147+
for _ in range(50):
148+
get(PageView, project=self.project, version=version)
149+
get(ImportedFile, project=self.project, version=version)
150+
get(SearchQuery, project=self.project, version=version)
151+
get(Build, project=self.project, version=version)
152+
153+
with self.assertNumQueries(48):
154+
self.project.delete()

0 commit comments

Comments
 (0)