Skip to content

Commit 6bb753a

Browse files
humitosagjohnson
andauthored
Celery: cheat job_status view to return finished after 5 polls (#10107)
* Celery: cheat `job_status` view to return `finished` after 5 polls The frontend hits this endpoint every 1s. As we don't know when the task has finished now since we disabled the Celery result backend, we are just cheating it and return finished after 5 times the view is called with the same `task_id`. This fixes the issue we have in the UI when clicking the circled arrows to re-sync the remote repositories. * Update readthedocs/api/v2/views/task_views.py Co-authored-by: Anthony <[email protected]> --------- Co-authored-by: Anthony <[email protected]>
1 parent bc8aa71 commit 6bb753a

File tree

6 files changed

+23
-139
lines changed

6 files changed

+23
-139
lines changed

readthedocs/api/v2/views/task_views.py

+19-35
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,45 @@
11
"""Endpoints relating to task/job status, etc."""
22

33
import structlog
4-
4+
from django.core.cache import cache
55
from django.urls import reverse
6-
from redis import ConnectionError
76
from rest_framework import decorators, permissions
87
from rest_framework.renderers import JSONRenderer
98
from rest_framework.response import Response
109

11-
from readthedocs.core.utils.tasks import TaskNoPermission, get_public_task_data
1210
from readthedocs.oauth import tasks
1311

14-
1512
log = structlog.get_logger(__name__)
1613

17-
SUCCESS_STATES = ('SUCCESS',)
18-
FAILURE_STATES = (
19-
'FAILURE',
20-
'REVOKED',
21-
)
22-
FINISHED_STATES = SUCCESS_STATES + FAILURE_STATES
23-
STARTED_STATES = ('RECEIVED', 'STARTED', 'RETRY') + FINISHED_STATES
24-
25-
26-
def get_status_data(task_name, state, data, error=None):
27-
data = {
28-
'name': task_name,
29-
'data': data,
30-
'started': state in STARTED_STATES,
31-
'finished': state in FINISHED_STATES,
32-
# When an exception is raised inside the task, we keep this as SUCCESS
33-
# and add the exception message into the 'error' key
34-
'success': state in SUCCESS_STATES and error is None,
35-
}
36-
if error is not None:
37-
data['error'] = error
38-
return data
39-
4014

4115
@decorators.api_view(['GET'])
4216
@decorators.permission_classes((permissions.AllowAny,))
4317
@decorators.renderer_classes((JSONRenderer,))
4418
def job_status(request, task_id):
45-
try:
46-
task_name, state, public_data, error = get_public_task_data(
47-
request,
48-
task_id,
49-
)
50-
except (TaskNoPermission, ConnectionError):
51-
return Response(get_status_data('unknown', 'PENDING', {}),)
52-
return Response(get_status_data(task_name, state, public_data, error),)
19+
"""Retrieve Celery task function state from frontend."""
20+
# HACK: always poll up to N times and after that return the sync has
21+
# finished. This is a way to avoid re-enabling Celery result backend for now.
22+
# TODO remove this API and RemoteRepo sync UI when we have better auto syncing
23+
poll_n = cache.get(task_id, 0)
24+
poll_n += 1
25+
cache.set(task_id, poll_n, 5 * 60)
26+
finished = poll_n == 5
27+
28+
data = {
29+
"name": "sync_remote_repositories",
30+
"data": {},
31+
"started": True,
32+
"finished": finished,
33+
"success": finished,
34+
}
35+
return Response(data)
5336

5437

5538
@decorators.api_view(['POST'])
5639
@decorators.permission_classes((permissions.IsAuthenticated,))
5740
@decorators.renderer_classes((JSONRenderer,))
5841
def sync_remote_repositories(request):
42+
"""Trigger a re-sync of remote repositories for the user."""
5943
result = tasks.sync_remote_repositories.delay(user_id=request.user.id,)
6044
task_id = result.task_id
6145
return Response({

readthedocs/core/utils/tasks/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,4 @@
66
)
77
from .public import PublicTask # noqa
88
from .public import TaskNoPermission # noqa
9-
from .public import get_public_task_data # noqa
109
from .retrieve import TaskNotFound # noqa
11-
from .retrieve import get_task_data # noqa

readthedocs/core/utils/tasks/public.py

+3-38
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
# -*- coding: utf-8 -*-
2-
31
"""Celery tasks with publicly viewable status."""
42

53
from celery import Task, states
64
from django.conf import settings
75

8-
from .retrieve import TaskNotFound, get_task_data
9-
10-
116
__all__ = (
127
'PublicTask',
138
'TaskNoPermission',
14-
'get_public_task_data',
159
)
1610

1711
STATUS_UPDATES_ENABLED = not settings.CELERY_ALWAYS_EAGER
1812

1913

14+
# pylint: disable=abstract-method
15+
# pylint: disable=broad-except
16+
# pylint: disable=invalid-name
2017
class PublicTask(Task):
2118

2219
"""
@@ -121,35 +118,3 @@ def __init__(self, task_id, *args, **kwargs):
121118
id=task_id,
122119
)
123120
super().__init__(message, *args, **kwargs)
124-
125-
126-
def get_public_task_data(request, task_id):
127-
"""
128-
Return task details as tuple.
129-
130-
Will raise `TaskNoPermission` if `request` has no permission to access info
131-
of the task with id `task_id`. This is also the case of no task with the
132-
given id exists.
133-
134-
:returns: (task name, task state, public data, error message)
135-
:rtype: (str, str, dict, str)
136-
"""
137-
try:
138-
task, state, info = get_task_data(task_id)
139-
except TaskNotFound:
140-
# No task info has been found act like we don't have permission to see
141-
# the results.
142-
raise TaskNoPermission(task_id)
143-
144-
if not hasattr(task, 'check_permission'):
145-
raise TaskNoPermission(task_id)
146-
147-
context = info.get('context', {})
148-
if not task.check_permission(request, state, context):
149-
raise TaskNoPermission(task_id)
150-
return (
151-
task.name,
152-
state,
153-
info.get('public_data', {}),
154-
info.get('error', None),
155-
)
+1-26
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,12 @@
1-
# -*- coding: utf-8 -*-
21

32
"""Utilities for retrieving task data."""
43

5-
from celery import states
6-
from celery.result import AsyncResult
74

8-
9-
__all__ = ('TaskNotFound', 'get_task_data')
5+
__all__ = ("TaskNotFound",)
106

117

128
class TaskNotFound(Exception):
139

1410
def __init__(self, task_id, *args, **kwargs):
1511
message = 'No public task found with id {id}'.format(id=task_id)
1612
super().__init__(message, *args, **kwargs)
17-
18-
19-
def get_task_data(task_id):
20-
"""
21-
Will raise `TaskNotFound` if the task is in state ``PENDING`` or the task.
22-
23-
meta data has no ``'task_name'`` key set.
24-
"""
25-
from readthedocs.worker import app
26-
27-
result = AsyncResult(task_id)
28-
state, info = result.state, result.info
29-
if state == states.PENDING:
30-
raise TaskNotFound(task_id)
31-
if 'task_name' not in info:
32-
raise TaskNotFound(task_id)
33-
try:
34-
task = app.tasks[info['task_name']]
35-
except KeyError:
36-
raise TaskNotFound(task_id)
37-
return task, state, info

readthedocs/rtd_tests/tests/test_api.py

-22
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
GitHubWebhookView,
3939
GitLabWebhookView,
4040
)
41-
from readthedocs.api.v2.views.task_views import get_status_data
4241
from readthedocs.builds.constants import (
4342
BUILD_STATE_CLONING,
4443
BUILD_STATE_FINISHED,
@@ -2545,24 +2544,3 @@ def test_modify_version(self):
25452544
self.assertEqual(resp.data['has_pdf'], True)
25462545
self.assertEqual(resp.data['has_epub'], False)
25472546
self.assertEqual(resp.data['has_htmlzip'], False)
2548-
2549-
2550-
class TaskViewsTests(TestCase):
2551-
2552-
def test_get_status_data(self):
2553-
data = get_status_data(
2554-
'public_task_exception',
2555-
'SUCCESS',
2556-
{'data': 'public'},
2557-
'Something bad happened',
2558-
)
2559-
self.assertEqual(
2560-
data, {
2561-
'name': 'public_task_exception',
2562-
'data': {'data': 'public'},
2563-
'started': True,
2564-
'finished': True,
2565-
'success': False,
2566-
'error': 'Something bad happened',
2567-
},
2568-
)

readthedocs/rtd_tests/tests/test_privacy_urls.py

-16
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
RegexAutomationRule,
1616
VersionAutomationRule,
1717
)
18-
from readthedocs.core.utils.tasks import TaskNoPermission
1918
from readthedocs.integrations.models import HttpExchange, Integration
2019
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
2120
from readthedocs.projects.models import Domain, EnvironmentVariable, Project, WebHook
@@ -452,21 +451,6 @@ def setUp(self):
452451
}
453452

454453

455-
class APIUnauthAccessTest(APIMixin, TestCase):
456-
457-
@mock.patch('readthedocs.api.v2.views.task_views.get_public_task_data')
458-
def test_api_urls(self, get_public_task_data):
459-
from readthedocs.api.v2.urls import urlpatterns
460-
get_public_task_data.side_effect = TaskNoPermission('Nope')
461-
self._test_url(urlpatterns)
462-
463-
def login(self):
464-
pass
465-
466-
def is_admin(self):
467-
return False
468-
469-
470454
class PublicUserProfileMixin(URLAccessMixin):
471455

472456
def setUp(self):

0 commit comments

Comments
 (0)