Skip to content

Commit 0c59910

Browse files
ericholscherstsewdhumitos
authored
Migrate sync_versions from an API call to a task (#7548)
This should stop the API from timing out, since we aren’t running all this logic in process. This is most important for projects with lots of versions, because this logic takes a long time to run and times out the web processes. It also eats up our API processes, causing other issues with our API because of blocking. To test this, you can use the `pull` management command on a project you have checked out locally: ``` inv -e docker.manage 'pull time-test’ ``` Co-authored-by: Santos Gallegos <[email protected]> Co-authored-by: Manuel Kaufmann <[email protected]>
1 parent 2b5ca31 commit 0c59910

File tree

9 files changed

+243
-154
lines changed

9 files changed

+243
-154
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ media/pdf
3939
media/static
4040
/static
4141
node_modules
42+
nodeenv
4243
readthedocs/rtd_tests/builds
4344
readthedocs/rtd_tests/tests/builds
4445
user_builds

readthedocs/api/v2/utils.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,16 @@ def _set_or_create_version(project, slug, version_id, verbose_name, type_):
166166
return version, False
167167

168168

169-
def _get_deleted_versions_qs(project, version_data):
169+
def _get_deleted_versions_qs(project, tags_data, branches_data):
170170
# We use verbose_name for tags
171171
# because several tags can point to the same identifier.
172172
versions_tags = [
173-
version['verbose_name'] for version in version_data.get('tags', [])
173+
version['verbose_name']
174+
for version in tags_data
174175
]
175176
versions_branches = [
176-
version['identifier'] for version in version_data.get('branches', [])
177+
version['identifier']
178+
for version in branches_data
177179
]
178180

179181
to_delete_qs = (
@@ -193,14 +195,18 @@ def _get_deleted_versions_qs(project, version_data):
193195
return to_delete_qs
194196

195197

196-
def delete_versions_from_db(project, version_data):
198+
def delete_versions_from_db(project, tags_data, branches_data):
197199
"""
198200
Delete all versions not in the current repo.
199201
200202
:returns: The slug of the deleted versions from the database.
201203
"""
202204
to_delete_qs = (
203-
_get_deleted_versions_qs(project, version_data)
205+
_get_deleted_versions_qs(
206+
project=project,
207+
tags_data=tags_data,
208+
branches_data=branches_data,
209+
)
204210
.exclude(active=True)
205211
)
206212
deleted_versions = set(to_delete_qs.values_list('slug', flat=True))
@@ -214,10 +220,14 @@ def delete_versions_from_db(project, version_data):
214220
return deleted_versions
215221

216222

217-
def get_deleted_active_versions(project, version_data):
223+
def get_deleted_active_versions(project, tags_data, branches_data):
218224
"""Return the slug of active versions that were deleted from the repository."""
219225
to_delete_qs = (
220-
_get_deleted_versions_qs(project, version_data)
226+
_get_deleted_versions_qs(
227+
project=project,
228+
tags_data=tags_data,
229+
branches_data=branches_data,
230+
)
221231
.filter(active=True)
222232
)
223233
return set(to_delete_qs.values_list('slug', flat=True))

readthedocs/api/v2/views/model_views.py

Lines changed: 18 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,21 @@
66
from allauth.socialaccount.models import SocialAccount
77
from django.conf import settings
88
from django.core.files.storage import get_storage_class
9-
from django.db.models import Q
109
from django.shortcuts import get_object_or_404
1110
from django.template.loader import render_to_string
1211
from rest_framework import decorators, permissions, status, viewsets
1312
from rest_framework.parsers import JSONParser, MultiPartParser
1413
from rest_framework.renderers import BaseRenderer, JSONRenderer
1514
from rest_framework.response import Response
1615

17-
from readthedocs.builds.constants import (
18-
BRANCH,
19-
BUILD_STATE_FINISHED,
20-
BUILD_STATE_TRIGGERED,
21-
INTERNAL,
22-
TAG,
23-
)
16+
from readthedocs.builds.constants import INTERNAL
2417
from readthedocs.builds.models import Build, BuildCommandResult, Version
25-
from readthedocs.core.utils import trigger_build
26-
from readthedocs.core.utils.extend import SettingsOverrideObject
18+
from readthedocs.builds.tasks import sync_versions_task
2719
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
2820
from readthedocs.oauth.services import GitHubService, registry
29-
from readthedocs.projects.models import Domain, EmailHook, Project
30-
from readthedocs.projects.version_handling import determine_stable_version
31-
32-
from ..permissions import (
33-
APIPermission,
34-
APIRestrictedPermission,
35-
IsOwner,
36-
RelatedProjectIsOwner,
37-
)
21+
from readthedocs.projects.models import Domain, Project
22+
23+
from ..permissions import APIPermission, APIRestrictedPermission, IsOwner
3824
from ..serializers import (
3925
BuildAdminSerializer,
4026
BuildCommandSerializer,
@@ -52,10 +38,6 @@
5238
ProjectPagination,
5339
RemoteOrganizationPagination,
5440
RemoteProjectPagination,
55-
delete_versions_from_db,
56-
get_deleted_active_versions,
57-
run_automation_rules,
58-
sync_versions_to_db,
5941
)
6042

6143
log = logging.getLogger(__name__)
@@ -181,47 +163,34 @@ def canonical_url(self, request, **kwargs):
181163
permission_classes=[permissions.IsAdminUser],
182164
methods=['post'],
183165
)
184-
def sync_versions(self, request, **kwargs): # noqa: D205
166+
def sync_versions(self, request, **kwargs): # noqa
185167
"""
186168
Sync the version data in the repo (on the build server).
187169
188170
Version data in the repo is synced with what we have in the database.
189171
190172
:returns: the identifiers for the versions that have been deleted.
173+
174+
.. note::
175+
176+
This endpoint is deprecated in favor of `sync_versions_task`.
191177
"""
192178
project = get_object_or_404(
193179
Project.objects.api(request.user),
194180
pk=kwargs['pk'],
195181
)
196182

197-
# If the currently highest non-prerelease version is active, then make
198-
# the new latest version active as well.
199-
current_stable = project.get_original_stable_version()
200-
if current_stable is not None:
201-
activate_new_stable = current_stable.active
202-
else:
203-
activate_new_stable = False
183+
added_versions = []
184+
deleted_versions = []
204185

205186
try:
206-
# Update All Versions
207187
data = request.data
208-
added_versions = set()
209-
if 'tags' in data:
210-
ret_set = sync_versions_to_db(
211-
project=project,
212-
versions=data['tags'],
213-
type=TAG,
214-
)
215-
added_versions.update(ret_set)
216-
if 'branches' in data:
217-
ret_set = sync_versions_to_db(
218-
project=project,
219-
versions=data['branches'],
220-
type=BRANCH,
221-
)
222-
added_versions.update(ret_set)
223-
deleted_versions = delete_versions_from_db(project, data)
224-
deleted_active_versions = get_deleted_active_versions(project, data)
188+
# Calling the task synchronically to keep backward compatibility
189+
added_versions, deleted_versions = sync_versions_task(
190+
project_pk=project.pk,
191+
tags_data=data.get('tags', []),
192+
branches_data=data.get('branches', []),
193+
)
225194
except Exception as e:
226195
log.exception('Sync Versions Error')
227196
return Response(
@@ -231,42 +200,6 @@ def sync_versions(self, request, **kwargs): # noqa: D205
231200
status=status.HTTP_400_BAD_REQUEST,
232201
)
233202

234-
try:
235-
# The order of added_versions isn't deterministic.
236-
# We don't track the commit time or any other metadata.
237-
# We usually have one version added per webhook.
238-
run_automation_rules(project, added_versions, deleted_active_versions)
239-
except Exception:
240-
# Don't interrupt the request if something goes wrong
241-
# in the automation rules.
242-
log.exception(
243-
'Failed to execute automation rules for [%s]: %s',
244-
project.slug, added_versions
245-
)
246-
247-
# TODO: move this to an automation rule
248-
promoted_version = project.update_stable_version()
249-
new_stable = project.get_stable_version()
250-
if promoted_version and new_stable and new_stable.active:
251-
log.info(
252-
'Triggering new stable build: %(project)s:%(version)s',
253-
{
254-
'project': project.slug,
255-
'version': new_stable.identifier,
256-
}
257-
)
258-
trigger_build(project=project, version=new_stable)
259-
260-
# Marking the tag that is considered the new stable version as
261-
# active and building it if it was just added.
262-
if (
263-
activate_new_stable and
264-
promoted_version.slug in added_versions
265-
):
266-
promoted_version.active = True
267-
promoted_version.save()
268-
trigger_build(project=project, version=promoted_version)
269-
270203
return Response({
271204
'added_versions': added_versions,
272205
'deleted_versions': deleted_versions,

readthedocs/builds/tasks.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@
88
from django.core.files.storage import get_storage_class
99

1010
from readthedocs.api.v2.serializers import BuildSerializer
11+
from readthedocs.api.v2.utils import (
12+
delete_versions_from_db,
13+
get_deleted_active_versions,
14+
run_automation_rules,
15+
sync_versions_to_db,
16+
)
1117
from readthedocs.builds.constants import (
18+
BRANCH,
1219
BUILD_STATUS_FAILURE,
1320
BUILD_STATUS_PENDING,
1421
BUILD_STATUS_SUCCESS,
1522
MAX_BUILD_COMMAND_SIZE,
23+
TAG,
1624
)
1725
from readthedocs.builds.models import Build, Version
1826
from readthedocs.builds.utils import memcache_lock
27+
from readthedocs.core.utils import trigger_build
28+
from readthedocs.projects.models import Project
1929
from readthedocs.projects.tasks import send_build_status
2030
from readthedocs.worker import app
2131

@@ -206,3 +216,99 @@ def delete_inactive_external_versions(limit=200, days=30 * 3):
206216
version.project.slug, version.slug,
207217
)
208218
version.delete()
219+
220+
221+
@app.task(
222+
max_retries=1,
223+
default_retry_delay=60,
224+
queue='web'
225+
)
226+
def sync_versions_task(project_pk, tags_data, branches_data, **kwargs):
227+
"""
228+
Sync the version data in the repo (from build server) into our database.
229+
230+
Creates new Version objects for tags/branches that aren't tracked in the database,
231+
and deletes Version objects for tags/branches that don't exists in the repository.
232+
233+
:param tags_data: List of dictionaries with ``verbose_name`` and ``identifier``.
234+
:param branches_data: Same as ``tags_data`` but for branches.
235+
:returns: the identifiers for the versions that have been deleted.
236+
"""
237+
project = Project.objects.get(pk=project_pk)
238+
239+
# If the currently highest non-prerelease version is active, then make
240+
# the new latest version active as well.
241+
current_stable = project.get_original_stable_version()
242+
if current_stable is not None:
243+
activate_new_stable = current_stable.active
244+
else:
245+
activate_new_stable = False
246+
247+
try:
248+
# Update All Versions
249+
added_versions = set()
250+
result = sync_versions_to_db(
251+
project=project,
252+
versions=tags_data,
253+
type=TAG,
254+
)
255+
added_versions.update(result)
256+
257+
result = sync_versions_to_db(
258+
project=project,
259+
versions=branches_data,
260+
type=BRANCH,
261+
)
262+
added_versions.update(result)
263+
264+
deleted_versions = delete_versions_from_db(
265+
project=project,
266+
tags_data=tags_data,
267+
branches_data=branches_data,
268+
)
269+
deleted_active_versions = get_deleted_active_versions(
270+
project=project,
271+
tags_data=tags_data,
272+
branches_data=branches_data,
273+
)
274+
except Exception:
275+
log.exception('Sync Versions Error')
276+
return [], []
277+
278+
try:
279+
# The order of added_versions isn't deterministic.
280+
# We don't track the commit time or any other metadata.
281+
# We usually have one version added per webhook.
282+
run_automation_rules(project, added_versions, deleted_active_versions)
283+
except Exception:
284+
# Don't interrupt the request if something goes wrong
285+
# in the automation rules.
286+
log.exception(
287+
'Failed to execute automation rules for [%s]: %s',
288+
project.slug, added_versions
289+
)
290+
291+
# TODO: move this to an automation rule
292+
promoted_version = project.update_stable_version()
293+
new_stable = project.get_stable_version()
294+
if promoted_version and new_stable and new_stable.active:
295+
log.info(
296+
'Triggering new stable build: %(project)s:%(version)s',
297+
{
298+
'project': project.slug,
299+
'version': new_stable.identifier,
300+
}
301+
)
302+
trigger_build(project=project, version=new_stable)
303+
304+
# Marking the tag that is considered the new stable version as
305+
# active and building it if it was just added.
306+
if (
307+
activate_new_stable and
308+
promoted_version.slug in added_versions
309+
):
310+
promoted_version.active = True
311+
promoted_version.save()
312+
trigger_build(project=project, version=promoted_version)
313+
314+
return list(added_versions), list(deleted_versions)

readthedocs/core/management/commands/pull.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66

7-
from django.core.management.base import BaseCommand
7+
from django.core.management.base import LabelCommand
88

99
from readthedocs.builds.constants import LATEST
1010
from readthedocs.projects import tasks, utils
@@ -13,12 +13,10 @@
1313
log = logging.getLogger(__name__)
1414

1515

16-
class Command(BaseCommand):
16+
class Command(LabelCommand):
1717

1818
help = __doc__
1919

20-
def handle(self, *args, **options):
21-
if args:
22-
for slug in args:
23-
version = utils.version_from_slug(slug, LATEST)
24-
tasks.sync_repository_task(version.pk)
20+
def handle_label(self, label, **options):
21+
version = utils.version_from_slug(label, LATEST)
22+
tasks.sync_repository_task(version.pk)

0 commit comments

Comments
 (0)