Skip to content

Commit bacef7e

Browse files
committed
Feedback, more testing, logic changes
* Removes some redundant calls to update build objects * Updates some docs * Logic behind update_build is improved to update in more cases * Tests for incremental updates
1 parent a9c2a22 commit bacef7e

File tree

5 files changed

+154
-63
lines changed

5 files changed

+154
-63
lines changed

readthedocs/doc_builder/environments.py

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -268,25 +268,39 @@ class BuildEnvironment(object):
268268
269269
Base class for wrapping command execution for build steps. This provides a
270270
context for command execution and reporting, and eventually performs updates
271-
on the build object itself, reporting success/failure, as well as top-level
272-
failures.
271+
on the build object itself, reporting success/failure, as well as failures
272+
during the context manager enter and exit.
273+
274+
Any exceptions raised inside this context and handled by the eventual
275+
:py:meth:`__exit__` method, specifically, inside :py:meth:`handle_exception`
276+
and :py:meth:`update_build`. If the exception is a subclass of
277+
:py:cls:`BuildEnvironmentError`, then this error message is added to the
278+
build object and is shown to the user as the top-level failure reason for
279+
why the build failed. Other exceptions raise a general failure warning on
280+
the build.
281+
282+
We only update the build through the API in one of three cases:
283+
284+
* The build is not done and needs incremental builds
285+
* The build failed and we should always report this change
286+
* The build was successful and ``commit`` is ``True``
273287
274288
:param project: Project that is being built
275289
:param version: Project version that is being built
276290
:param build: Build instance
277291
:param record: Record status of build object
278292
:param environment: shell environment variables
279-
:param finalize: finalize the build by setting a finished state on exit
293+
:param commit: update the build object via API if the build was successful
280294
"""
281295

282296
def __init__(self, project=None, version=None, build=None, record=True,
283-
environment=None, finalize=True):
297+
environment=None, commit=True):
284298
self.project = project
285299
self.version = version
286300
self.build = build
287301
self.record = record
288302
self.environment = environment or {}
289-
self.finalize = finalize
303+
self.commit = commit
290304

291305
self.commands = []
292306
self.failure = None
@@ -389,9 +403,12 @@ def done(self):
389403
def update_build(self, state=None):
390404
"""Record a build by hitting the API
391405
392-
This step is skipped if we aren't recording the build, or if we don't
393-
want to record successful builds yet (if we are running setup commands
394-
for the build)
406+
This step is skipped if we aren't recording the build. To avoid
407+
recording successful builds yet (for instance, running setup commands for
408+
the build), set the ``commit`` argument on environment instantiation.
409+
410+
If there was an error on the build, update the build regardless of
411+
whether ``commit`` is ``True`` or not.
395412
"""
396413
if not self.record:
397414
return None
@@ -420,32 +437,41 @@ def update_build(self, state=None):
420437
self.build['length'] = int(build_length.total_seconds())
421438

422439
if self.failure is not None:
423-
# Only surface the error message if it was a
424-
# BuildEnvironmentException or BuildEnvironmentWarning
425-
if isinstance(self.failure,
426-
(BuildEnvironmentException, BuildEnvironmentWarning)):
427-
self.build['error'] = str(self.failure)
428-
else:
429-
self.build['error'] = ugettext_noop(
430-
"There was a problem with Read the Docs while building your documentation. "
431-
"Please report this to us with your build id ({build_id}).".format(
432-
build_id=self.build['id']
433-
)
434-
)
440+
# Surface a generic error if the class is not a
441+
# BuildEnvironmentError
442+
if not isinstance(self.failure,
443+
(BuildEnvironmentException,
444+
BuildEnvironmentWarning)):
435445
log.error(
436446
'Build failed with unhandled exception: %s',
437447
str(self.failure),
438-
extra={'stack': True,
439-
'tags': {'build': self.build['id']},
440-
}
448+
extra={
449+
'stack': True,
450+
'tags': {'build': self.build['id']},
451+
}
452+
)
453+
self.failure = BuildEnvironmentError(
454+
BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(
455+
build_id=self.build['id'],
456+
)
441457
)
458+
self.build['error'] = str(self.failure)
442459

443460
# Attempt to stop unicode errors on build reporting
444461
for key, val in list(self.build.items()):
445462
if isinstance(val, six.binary_type):
446463
self.build[key] = val.decode('utf-8', 'ignore')
447464

448-
if self.finalize:
465+
# We are selective about when we update the build object here
466+
update_build = (
467+
# Build isn't done yet, we unconditionally update in this state
468+
not self.done
469+
# Build is done, but isn't successful, always update
470+
or (self.done and not self.successful)
471+
# Otherwise, are we explicitly to not update?
472+
or self.commit
473+
)
474+
if update_build:
449475
try:
450476
api_v2.build(self.build['id']).put(self.build)
451477
except HttpClientError as e:
@@ -556,12 +582,15 @@ def __exit__(self, exc_type, exc_value, tb):
556582
try:
557583
client.kill(self.container_id)
558584
except DockerAPIError:
559-
pass
585+
log.exception(
586+
'Unable to remove container: id=%s',
587+
self.container_id,
588+
)
560589
try:
561-
log.info('Removing container %s', self.container_id)
590+
log.info('Removing container: id=%s', self.container_id)
562591
client.remove_container(self.container_id)
563-
# Catch direct failures from Docker API, but also requests exceptions
564-
# with the HTTP request. These should not
592+
# Catch direct failures from Docker API or with a requests HTTP
593+
# request. These errors should not surface to the user.
565594
except (DockerAPIError, ConnectionError):
566595
log.exception(
567596
LOG_TEMPLATE
@@ -599,13 +628,21 @@ def get_client(self):
599628
)
600629
return self.client
601630
except DockerException as e:
602-
log.error(LOG_TEMPLATE
603-
.format(
604-
project=self.project.slug,
605-
version=self.version.slug,
606-
msg=e),
607-
exc_info=True)
608-
raise BuildEnvironmentError('Problem creating build environment')
631+
log.exception(
632+
LOG_TEMPLATE.format(
633+
project=self.project.slug,
634+
version=self.version.slug,
635+
msg='Could not connection to Docker API',
636+
),
637+
)
638+
# We don't raise an error here mentioning Docker, that is a
639+
# technical detail that the user can't resolve on their own.
640+
# Instead, give the user a generic failure
641+
raise BuildEnvironmentError(
642+
BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(
643+
build_id=self.build['id'],
644+
)
645+
)
609646

610647
@property
611648
def container_id(self):

readthedocs/doc_builder/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Exceptions raised when building documentation."""
22

3+
from django.utils.translation import ugettext_noop
4+
35

46
class BuildEnvironmentException(Exception):
57

@@ -15,7 +17,11 @@ def get_default_message(self):
1517

1618

1719
class BuildEnvironmentError(BuildEnvironmentException):
18-
pass
20+
21+
GENERIC_WITH_BUILD_ID = ugettext_noop(
22+
"There was a problem with Read the Docs while building your documentation. "
23+
"Please report this to us with your build id ({build_id})."
24+
)
1925

2026

2127
class BuildEnvironmentWarning(BuildEnvironmentException):

readthedocs/projects/exceptions.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ class RepositoryError(BuildEnvironmentError):
2020

2121
"""Failure during repository operation."""
2222

23-
PRIVATE_REPO = _(
23+
PRIVATE_ALLOWED = _(
2424
'There was a problem connecting to your repository, '
2525
'ensure that your repository URL is correct.'
2626
)
27-
PUBLIC_REPO = _(
27+
PRIVATE_NOT_ALLOWED = _(
2828
'There was a problem connecting to your repository, '
29-
'ensure that your repository URL is correct and your repository is public.'
29+
'ensure that your repository URL is correct and your repository is public. '
30+
'Private repositories are not supported.'
3031
)
3132

3233
def get_default_message(self):
3334
if settings.ALLOW_PRIVATE_REPOS:
34-
return self.PRIVATE_REPO
35-
return self.PUBLIC_REPO
35+
return self.PRIVATE_ALLOWED
36+
return self.PRIVATE_NOT_ALLOWED
3637

3738

3839
class ProjectSpamError(Exception):

readthedocs/projects/tasks.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class UpdateDocsTask(Task):
7575
Whether or not to keep a record of the update in the database. Useful
7676
for preventing changes visible to the end-user when running commands
7777
from the shell, for example.
78-
7978
"""
8079

8180
max_retries = 5
@@ -134,11 +133,11 @@ def run(self, pk, version_pk=None, build_pk=None, record=True,
134133
'An unhandled exception was raised during build setup',
135134
extra={'tags': {'build': build_pk}}
136135
)
137-
self.setup_env.build['error'] = _(
138-
'An unknown error was encountered while setting up your project. '
139-
'Please include the build id ({build_id}) in any bug reports.'.format(
140-
build_id=build_pk
141-
))
136+
self.setup_env.failure = BuildEnvironmentError(
137+
BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(
138+
build_id=build_pk,
139+
)
140+
)
142141
self.setup_env.update_build(BUILD_STATE_FINISHED)
143142
return False
144143
else:
@@ -151,11 +150,11 @@ def run(self, pk, version_pk=None, build_pk=None, record=True,
151150
'An unhandled exception was raised during project build',
152151
extra={'tags': {'build': build_pk}}
153152
)
154-
self.build_env.build['error'] = _(
155-
'An unknown error was encountered while building your project. '
156-
'Please include the build id ({build_id}) in any bug reports.'.format(
157-
build_id=build_pk
158-
))
153+
self.build_env.failure = BuildEnvironmentError(
154+
BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(
155+
build_id=build_pk,
156+
)
157+
)
159158
self.build_env.update_build(BUILD_STATE_FINISHED)
160159
return False
161160

@@ -165,14 +164,13 @@ def run_setup(self, record=True):
165164
"""Run setup in the local environment.
166165
167166
Return True if successful.
168-
169167
"""
170168
self.setup_env = LocalEnvironment(
171169
project=self.project,
172170
version=self.version,
173171
build=self.build,
174172
record=record,
175-
finalize=False,
173+
commit=False,
176174
)
177175

178176
# Environment used for code checkout & initial configuration reading
@@ -205,7 +203,6 @@ def run_setup(self, record=True):
205203
if not isinstance(self.setup_env.failure, vcs_support_utils.LockTimeout):
206204
self.send_notifications()
207205

208-
self.setup_env.update_build(state=BUILD_STATE_FINISHED)
209206
return False
210207

211208
if self.setup_env.successful and not self.project.has_valid_clone:
@@ -268,7 +265,6 @@ def run_build(self, docker=False, record=True):
268265
if self.build_env.failed:
269266
self.send_notifications()
270267

271-
self.build_env.update_build(state=BUILD_STATE_FINISHED)
272268
build_complete.send(sender=Build, build=self.build_env.build)
273269

274270
@staticmethod

0 commit comments

Comments
 (0)