Repository: readthedocs/readthedocs.org

Issue:
Double build environments cause premature marking of build complete
With the two separate runs of the build environments on the build task, the build gets marked as complete by the `LocalEnvironment` pass. Environment creation should include a way to override reporting the build state.



Discussion:

Resolving patch:
diff --git a/readthedocs/doc_builder/constants.py b/readthedocs/doc_builder/constants.py
--- a/readthedocs/doc_builder/constants.py
+++ b/readthedocs/doc_builder/constants.py
@@ -23,3 +23,5 @@
 
 DOCKER_TIMEOUT_EXIT_CODE = 42
 DOCKER_OOM_EXIT_CODE = 137
+
+DOCKER_HOSTNAME_MAX_LEN = 64
diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py
--- a/readthedocs/doc_builder/environments.py
+++ b/readthedocs/doc_builder/environments.py
@@ -30,7 +30,7 @@
 from .constants import (DOCKER_SOCKET, DOCKER_VERSION, DOCKER_IMAGE,
                         DOCKER_LIMITS, DOCKER_TIMEOUT_EXIT_CODE,
                         DOCKER_OOM_EXIT_CODE, SPHINX_TEMPLATE_DIR,
-                        MKDOCS_TEMPLATE_DIR)
+                        MKDOCS_TEMPLATE_DIR, DOCKER_HOSTNAME_MAX_LEN)
 
 log = logging.getLogger(__name__)
 
@@ -250,23 +250,29 @@ def get_wrapped_command(self):
 
 class BuildEnvironment(object):
 
-    '''
-    Base build environment
+    """Base build environment
 
-    Placeholder for reorganizing command execution.
+    Base class for wrapping command execution for build steps. This provides a
+    context for command execution and reporting, and eventually performs updates
+    on the build object itself, reporting success/failure, as well as top-level
+    failures.
 
     :param project: Project that is being built
     :param version: Project version that is being built
     :param build: Build instance
     :param record: Record status of build object
-    '''
+    :param environment: shell environment variables
+    :param report_build_success: update build if successful
+    """
 
-    def __init__(self, project=None, version=None, build=None, record=True, environment=None):
+    def __init__(self, project=None, version=None, build=None, record=True,
+                 environment=None, report_build_success=True):
         self.project = project
         self.version = version
         self.build = build
         self.record = record
         self.environment = environment or {}
+        self.report_build_success = report_build_success
 
         self.commands = []
         self.failure = None
@@ -372,12 +378,14 @@ def done(self):
                 self.build['state'] == BUILD_STATE_FINISHED)
 
     def update_build(self, state=None):
-        """
-        Record a build by hitting the API.
+        """Record a build by hitting the API
 
-        Returns nothing
+        This step is skipped if we aren't recording the build, or if we don't
+        want to record successful builds yet (if we are running setup commands
+        for the build)
         """
-        if not self.record:
+        if not self.record or (state == BUILD_STATE_FINISHED and
+                               not self.report_build_success):
             return None
 
         self.build['project'] = self.project.pk
@@ -448,9 +456,13 @@ def __init__(self, *args, **kwargs):
         super(DockerEnvironment, self).__init__(*args, **kwargs)
         self.client = None
         self.container = None
-        self.container_name = None
-        if self.version:
-            self.container_name = slugify(unicode(self.version))
+        self.container_name = slugify(
+            'build-{build}-project-{project_id}-{project_name}'.format(
+                build=self.build.get('id'),
+                project_id=self.project.pk,
+                project_name=self.project.slug,
+            )[:DOCKER_HOSTNAME_MAX_LEN]
+        )
         if self.project.container_mem_limit:
             self.container_mem_limit = self.project.container_mem_limit
         if self.project.container_time_limit:
diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py
--- a/readthedocs/projects/tasks.py
+++ b/readthedocs/projects/tasks.py
@@ -113,7 +113,8 @@ def run(self, pk, version_pk=None, build_pk=None, record=True, docker=False,
 
         env_cls = LocalEnvironment
         self.setup_env = env_cls(project=self.project, version=self.version,
-                                 build=self.build, record=record)
+                                 build=self.build, record=record,
+                                 report_build_success=False)
 
         # Environment used for code checkout & initial configuration reading
         with self.setup_env:


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py
--- a/readthedocs/rtd_tests/tests/test_doc_building.py
+++ b/readthedocs/rtd_tests/tests/test_doc_building.py
@@ -116,9 +116,9 @@ def tearDown(self):
     def test_container_id(self):
         '''Test docker build command'''
         docker = DockerEnvironment(version=self.version, project=self.project,
-                                   build={})
+                                   build={'id': 123})
         self.assertEqual(docker.container_id,
-                         'version-foobar-of-pip-20')
+                         'build-123-project-6-pip')
 
     def test_connection_failure(self):
         '''Connection failure on to docker socket should raise exception'''
@@ -163,12 +163,12 @@ def test_command_execution(self):
         })
 
         build_env = DockerEnvironment(version=self.version, project=self.project,
-                                      build={})
+                                      build={'id': 123})
         with build_env:
             build_env.run('echo test', cwd='/tmp')
 
         self.mocks.docker_client.exec_create.assert_called_with(
-            container='version-foobar-of-pip-20',
+            container='build-123-project-6-pip',
             cmd="/bin/sh -c 'cd /tmp && echo\\ test'",
             stderr=True,
             stdout=True
@@ -193,12 +193,12 @@ def test_command_execution_cleanup_exception(self):
         })
 
         build_env = DockerEnvironment(version=self.version, project=self.project,
-                                      build={})
+                                      build={'id': 123})
         with build_env:
             build_env.run('echo', 'test', cwd='/tmp')
 
         self.mocks.docker_client.kill.assert_called_with(
-            'version-foobar-of-pip-20')
+            'build-123-project-6-pip')
         self.assertTrue(build_env.successful)
 
     def test_container_already_exists(self):