Skip to content

Commit 1b6860a

Browse files
authored
Merge pull request #3247 from rtfd/improve-task-error-reporting
Improve error reporting to users
2 parents c7e4f53 + 64ce4b4 commit 1b6860a

File tree

6 files changed

+64
-40
lines changed

6 files changed

+64
-40
lines changed

readthedocs/doc_builder/environments.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,11 @@ def update_build(self, state=None):
421421
self.build['error'] = str(self.failure)
422422
else:
423423
self.build['error'] = ugettext_noop(
424-
"An unexpected error occurred")
424+
"There was a problem with Read the Docs while building your documentation. "
425+
"Please report this to us with your build id ({build_id}).".format(
426+
build_id=self.build['pk']
427+
)
428+
)
425429

426430
# Attempt to stop unicode errors on build reporting
427431
for key, val in list(self.build.items()):

readthedocs/projects/tasks.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,48 @@ def _log(self, msg):
108108
version=self.version.slug,
109109
msg=msg))
110110

111+
# pylint: disable=arguments-differ
111112
def run(self, pk, version_pk=None, build_pk=None, record=True,
112113
docker=False, search=True, force=False, localmedia=True, **__):
113-
# pylint: disable=arguments-differ
114-
self.project = self.get_project(pk)
115-
self.version = self.get_version(self.project, version_pk)
116-
self.build = self.get_build(build_pk)
117-
self.build_search = search
118-
self.build_localmedia = localmedia
119-
self.build_force = force
120-
self.config = None
114+
"""
115+
Run a documentation build.
121116
122-
setup_successful = self.run_setup(record=record)
123-
if setup_successful:
124-
self.run_build(record=record, docker=docker)
117+
This is fully wrapped in exception handling to account for a number of failure cases.
118+
"""
119+
try:
120+
self.project = self.get_project(pk)
121+
self.version = self.get_version(self.project, version_pk)
122+
self.build = self.get_build(build_pk)
123+
self.build_search = search
124+
self.build_localmedia = localmedia
125+
self.build_force = force
126+
self.config = None
127+
128+
setup_successful = self.run_setup(record=record)
129+
if setup_successful:
130+
self.run_build(record=record, docker=docker)
131+
failure = self.setup_env.failure or self.build_env.failure
132+
except Exception as e: # noqa
133+
log.exception('Top-level build exception has been raised', extra={'build': build_pk})
134+
failure = str(e)
135+
136+
# **Always** report build status.
137+
# This can still fail if the API Is totally down, but should catch more failures
138+
result = {}
139+
error = _('Unknown error encountered. '
140+
'Please include the build id ({build_id}) in any bug reports.'.format(
141+
build_id=build_pk
142+
))
143+
build_updates = {'state': BUILD_STATE_FINISHED}
144+
build_data = {}
145+
if hasattr(self, 'build'):
146+
build_data.update(self.build)
147+
if failure:
148+
build_updates['success'] = False
149+
build_updates['error'] = error
150+
build_data.update(build_updates)
151+
result = api_v2.build(build_pk).patch(build_updates)
152+
return result
125153

126154
def run_setup(self, record=True):
127155
"""Run setup in the local environment.
@@ -166,7 +194,6 @@ def run_setup(self, record=True):
166194
if not isinstance(self.setup_env.failure, vcs_support_utils.LockTimeout):
167195
self.send_notifications()
168196

169-
self.setup_env.update_build(state=BUILD_STATE_FINISHED)
170197
return False
171198

172199
if self.setup_env.successful and not self.project.has_valid_clone:
@@ -230,14 +257,11 @@ def run_build(self, docker=False, record=True):
230257
self.send_notifications()
231258
build_complete.send(sender=Build, build=self.build_env.build)
232259

233-
self.build_env.update_build(state=BUILD_STATE_FINISHED)
234-
235260
@staticmethod
236261
def get_project(project_pk):
237262
"""Get project from API"""
238263
project_data = api_v2.project(project_pk).get()
239-
project = APIProject(**project_data)
240-
return project
264+
return APIProject(**project_data)
241265

242266
@staticmethod
243267
def get_version(project, version_pk):

readthedocs/restapi/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55

66
from slumber import API, serialize
7-
from requests import Session
7+
import requests
88
from django.conf import settings
99
from rest_framework.renderers import JSONRenderer
1010
from rest_framework.parsers import JSONParser
@@ -32,8 +32,10 @@ def dumps(self, data):
3232

3333

3434
def setup_api():
35-
session = Session()
35+
session = requests.Session()
3636
session.headers.update({'Host': PRODUCTION_DOMAIN})
37+
retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
38+
session.mount(API_HOST, retry_adapter)
3739
api_config = {
3840
'base_url': '%s/api/v2/' % API_HOST,
3941
'serializer': serialize.Serializer(

readthedocs/rtd_tests/tests/test_celery.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ def test_clear_artifacts(self):
6464
self.assertTrue(result.successful())
6565
self.assertFalse(exists(directory))
6666

67-
@patch('readthedocs.projects.tasks.UpdateDocsTask.build_docs',
68-
new=MagicMock)
69-
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_vcs',
70-
new=MagicMock)
67+
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_environment', new=MagicMock)
68+
@patch('readthedocs.projects.tasks.UpdateDocsTask.build_docs', new=MagicMock)
69+
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_vcs', new=MagicMock)
7170
def test_update_docs(self):
7271
build = get(Build, project=self.project,
7372
version=self.project.versions.first())
@@ -80,11 +79,11 @@ def test_update_docs(self):
8079
intersphinx=False)
8180
self.assertTrue(result.successful())
8281

83-
@patch('readthedocs.projects.tasks.UpdateDocsTask.build_docs',
84-
new=MagicMock)
82+
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_environment', new=MagicMock)
83+
@patch('readthedocs.projects.tasks.UpdateDocsTask.build_docs', new=MagicMock)
84+
@patch('readthedocs.doc_builder.environments.BuildEnvironment.update_build', new=MagicMock)
8585
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_vcs')
86-
@patch('readthedocs.doc_builder.environments.BuildEnvironment.update_build')
87-
def test_update_docs_unexpected_setup_exception(self, mock_update_build, mock_setup_vcs):
86+
def test_update_docs_unexpected_setup_exception(self, mock_setup_vcs):
8887
exc = Exception()
8988
mock_setup_vcs.side_effect = exc
9089
build = get(Build, project=self.project,
@@ -97,13 +96,12 @@ def test_update_docs_unexpected_setup_exception(self, mock_update_build, mock_se
9796
record=False,
9897
intersphinx=False)
9998
self.assertTrue(result.successful())
100-
mock_update_build.assert_called_once_with(state=BUILD_STATE_FINISHED)
10199

100+
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_environment', new=MagicMock)
101+
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_vcs', new=MagicMock)
102+
@patch('readthedocs.doc_builder.environments.BuildEnvironment.update_build', new=MagicMock)
102103
@patch('readthedocs.projects.tasks.UpdateDocsTask.build_docs')
103-
@patch('readthedocs.projects.tasks.UpdateDocsTask.setup_vcs',
104-
new=MagicMock)
105-
@patch('readthedocs.doc_builder.environments.BuildEnvironment.update_build')
106-
def test_update_docs_unexpected_build_exception(self, mock_update_build, mock_build_docs):
104+
def test_update_docs_unexpected_build_exception(self, mock_build_docs):
107105
exc = Exception()
108106
mock_build_docs.side_effect = exc
109107
build = get(Build, project=self.project,
@@ -116,7 +114,6 @@ def test_update_docs_unexpected_build_exception(self, mock_update_build, mock_bu
116114
record=False,
117115
intersphinx=False)
118116
self.assertTrue(result.successful())
119-
mock_update_build.assert_called_with(state=BUILD_STATE_FINISHED)
120117

121118
def test_update_imported_doc(self):
122119
with mock_api(self.repo):

readthedocs/settings/base.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,15 +355,14 @@ def INSTALLED_APPS(self): # noqa
355355
},
356356
},
357357
'loggers': {
358-
'readthedocs': {
358+
'': { # root logger
359359
'handlers': ['debug', 'console'],
360-
'level': 'DEBUG',
361-
'propagate': True,
360+
'level': 'DEBUG', # Always send from the root, handlers can filter levels
362361
},
363-
'': {
362+
'readthedocs': {
364363
'handlers': ['debug', 'console'],
365364
'level': 'DEBUG',
366-
'propagate': True,
365+
'propagate': False, # Don't double log at the root logger for these.
367366
},
368367
},
369368
}

readthedocs/settings/dev.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ def DATABASES(self): # noqa
5656
def LOGGING(self): # noqa - avoid pep8 N802
5757
logging = super(CommunityDevSettings, self).LOGGING
5858
logging['formatters']['default']['format'] = '[%(asctime)s] ' + self.LOG_FORMAT
59-
# Remove double logging
60-
logging['loggers']['']['handlers'] = []
6159
return logging
6260

6361

0 commit comments

Comments
 (0)