Skip to content

Commit 2993075

Browse files
humitosxrmx
authored andcommitted
Handle raising exceptions from PublicTask (readthedocs#4078)
* Handle raising exceptions from PublicTask When the task raises an exception there is no way to have a custom result under ``AsyncResult.info``, it will be always the Exception that was risen from inside the task. Because of that, when the task raises an Exception we are handling it inside the ``run`` method and we add the exception's message into our custom result *without marking* the task as FAILURE. * Test case for a PublicTask that raises an Exception * Test case for method used to check a job status
1 parent 19a6772 commit 2993075

File tree

4 files changed

+74
-14
lines changed

4 files changed

+74
-14
lines changed

readthedocs/core/utils/tasks/public.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,28 @@ def set_public_data(self, data):
6767
self.update_progress_data()
6868

6969
def run(self, *args, **kwargs):
70+
error = False
71+
exception_raised = None
7072
self.set_permission_context(kwargs)
71-
result = self.run_public(*args, **kwargs)
72-
if result is not None:
73-
self.set_public_data(result)
74-
_, info = self.get_task_data()
75-
return info
73+
try:
74+
result = self.run_public(*args, **kwargs)
75+
except Exception as e:
76+
# With Celery 4 we lost the ability to keep our data dictionary into
77+
# ``AsyncResult.info`` when an exception was raised inside the
78+
# Task. In this case, ``info`` will contain the exception raised
79+
# instead of our data. So, I'm keeping the task as ``SUCCESS`` but
80+
# the adding the exception message into an ``error`` key to be used
81+
# from outside
82+
exception_raised = e
83+
error = True
7684

77-
def after_return(self, status, retval, task_id, args, kwargs, einfo):
78-
"""Add the error to the task data"""
7985
_, info = self.get_task_data()
80-
if status == states.FAILURE:
81-
info['error'] = retval
82-
if STATUS_UPDATES_ENABLED:
83-
self.update_state(state=status, meta=info)
86+
if error and exception_raised:
87+
info['error'] = str(exception_raised)
88+
elif result is not None:
89+
self.set_public_data(result)
90+
91+
return info
8492

8593

8694
def permission_check(check):

readthedocs/restapi/views/task_views.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ def get_status_data(task_name, state, data, error=None):
2929
'data': data,
3030
'started': state in STARTED_STATES,
3131
'finished': state in FINISHED_STATES,
32-
'success': state in SUCCESS_STATES,
32+
# When an exception is raised inside the task, we keep this as SUCCESS
33+
# and add the exception messsage into the 'error' key
34+
'success': state in SUCCESS_STATES and error is None,
3335
}
34-
if error is not None and isinstance(error, Exception):
35-
data['error'] = error.message
36+
if error is not None:
37+
data['error'] = error
3638
return data
3739

3840

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from readthedocs.integrations.models import Integration
2222
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
2323
from readthedocs.projects.models import Feature, Project
24+
from readthedocs.restapi.views.task_views import get_status_data
2425

2526
super_auth = base64.b64encode(b'super:test').decode('utf-8')
2627
eric_auth = base64.b64encode(b'eric:test').decode('utf-8')
@@ -759,3 +760,22 @@ def test_get_version_by_id(self):
759760
resp.data,
760761
version_data,
761762
)
763+
764+
765+
class TaskViewsTests(TestCase):
766+
767+
def test_get_status_data(self):
768+
data = get_status_data(
769+
'public_task_exception',
770+
'SUCCESS',
771+
{'data': 'public'},
772+
'Something bad happened',
773+
)
774+
self.assertEqual(data, {
775+
'name': 'public_task_exception',
776+
'data': {'data': 'public'},
777+
'started': True,
778+
'finished': True,
779+
'success': False,
780+
'error': 'Something bad happened',
781+
})

readthedocs/rtd_tests/tests/test_celery.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,33 @@ def test_sync_repository(self):
123123
args=(version.pk,),
124124
)
125125
self.assertTrue(result.successful())
126+
127+
def test_public_task_exception(self):
128+
"""
129+
Test when a PublicTask rises an Exception.
130+
131+
The exception should be catched and added to the ``info`` attribute of
132+
the result. Besides, the task should be SUCCESS.
133+
"""
134+
from readthedocs.core.utils.tasks import PublicTask
135+
from readthedocs.worker import app
136+
137+
class PublicTaskException(PublicTask):
138+
name = 'public_task_exception'
139+
140+
def run_public(self):
141+
raise Exception('Something bad happened')
142+
143+
app.tasks.register(PublicTaskException)
144+
exception_task = PublicTaskException()
145+
result = exception_task.delay()
146+
147+
# although the task risen an exception, it's success since we add the
148+
# exception into the ``info`` attributes
149+
self.assertEqual(result.status, 'SUCCESS')
150+
self.assertEqual(result.info, {
151+
'task_name': 'public_task_exception',
152+
'context': {},
153+
'public_data': {},
154+
'error': 'Something bad happened',
155+
})

0 commit comments

Comments
 (0)