Skip to content

Commit 3f072f5

Browse files
committed
De-duplicate builds
When a build is triggered, it could be marked as to be skipped by builders if: * there is already a build running/queued for the same commit Multiple builds of the same commit will lead to the same results. So, we skip it if there is one already running/queued. * there is already a build queued for the same version When building a version without specifying the commit, the last commit is built. In this case, we can only skip the new triggered build if there is one build queued because both builds will just pick the same commit.
1 parent 33d246c commit 3f072f5

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

readthedocs/core/utils/__init__.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020
from readthedocs.doc_builder.constants import DOCKER_LIMITS
2121
from readthedocs.projects.constants import CELERY_LOW, CELERY_MEDIUM, CELERY_HIGH
22-
from readthedocs.doc_builder.exceptions import BuildMaxConcurrencyError
22+
from readthedocs.doc_builder.exceptions import BuildMaxConcurrencyError, DuplicatedBuildError
2323

2424

2525
log = logging.getLogger(__name__)
@@ -165,8 +165,42 @@ def prepare_build(
165165
# External builds should be lower priority.
166166
options['priority'] = CELERY_LOW
167167

168+
skip_build = False
169+
if commit:
170+
skip_build = (
171+
Build.objects
172+
.filter(
173+
project=project,
174+
version=version,
175+
commit=commit,
176+
).exclude(
177+
state=BUILD_STATE_FINISHED,
178+
).exists()
179+
)
180+
else:
181+
skip_build = Build.objects.filter(
182+
project=project,
183+
version=version,
184+
state=BUILD_STATE_TRIGGERED,
185+
).count() > 0
186+
if skip_build:
187+
# TODO: we could mark the old build as duplicated, however we reset our
188+
# position in the queue and go back to the end of it --penalization
189+
log.warning(
190+
'Marking build to be skipped by builder. project=%s version=%s build=%s commit=%s',
191+
project.slug,
192+
version.slug,
193+
build.pk,
194+
commit,
195+
)
196+
build.error = DuplicatedBuildError.message
197+
build.success = False
198+
build.exit_code = 1
199+
build.state = BUILD_STATE_FINISHED
200+
build.save()
201+
168202
# Start the build in X minutes and mark it as limited
169-
if project.has_feature(Feature.LIMIT_CONCURRENT_BUILDS):
203+
if not skip_build and project.has_feature(Feature.LIMIT_CONCURRENT_BUILDS):
170204
running_builds = (
171205
Build.objects
172206
.filter(project__slug=project.slug)

readthedocs/doc_builder/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class BuildMaxConcurrencyError(BuildEnvironmentError):
5757
message = ugettext_noop('Concurrency limit reached ({limit}), retrying in 5 minutes.')
5858

5959

60+
class DuplicatedBuildError(BuildEnvironmentError):
61+
message = ugettext_noop('Duplicated build.')
62+
63+
6064
class BuildEnvironmentWarning(BuildEnvironmentException):
6165
pass
6266

readthedocs/projects/tasks.py

+11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
BuildEnvironmentWarning,
5959
BuildMaxConcurrencyError,
6060
BuildTimeoutError,
61+
DuplicatedBuildError,
6162
MkDocsYAMLParseError,
6263
ProjectBuildsSkippedError,
6364
VersionLockedError,
@@ -542,6 +543,16 @@ def run(
542543
self.commit = commit
543544
self.config = None
544545

546+
if self.build['state'] == BUILD_STATE_FINISHED and self.build['error'] == DuplicatedBuildError.message:
547+
log.warning(
548+
'NOOP: build is marked as duplicated. project=%s version=%s build=%s commit=%s',
549+
self.project.slug,
550+
self.version.slug,
551+
build_pk,
552+
self.commit,
553+
)
554+
return True
555+
545556
if self.project.has_feature(Feature.LIMIT_CONCURRENT_BUILDS):
546557
response = api_v2.build.running.get(project__slug=self.project.slug)
547558
builds_running = response.get('count', 0)

0 commit comments

Comments
 (0)