Skip to content

Commit 1eba499

Browse files
committed
Build: cancel old builds
This commit implements a simple logic to cancel old running builds when a new build for the same project/version arrives: 1. look for running builds for the same project/version 2. if there are any, it cancels them all one by one via Celery's revoke method 3. trigger a new build for the current commit received Note that this new feature is behind a feature flag (`CANCEL_OLD_BUILDS`) for now so we can start testing it on some projects that have shown their interest on this feature. The current behavior for `DEDUPLICATE_BUILDS` will be replaced by this new logic in the future. However, it was not removed in this commit since it's still useful for projects that won't be using the new feature flag yet. Closes #8961
1 parent b68a2a3 commit 1eba499

File tree

2 files changed

+82
-45
lines changed

2 files changed

+82
-45
lines changed

readthedocs/core/utils/__init__.py

+69-39
Original file line numberDiff line numberDiff line change
@@ -124,56 +124,86 @@ def prepare_build(
124124
)
125125

126126
skip_build = False
127-
if commit:
128-
skip_build = (
127+
# Reduce overhead when doing multiple push on the same version.
128+
if project.has_feature(Feature.CANCEL_OLD_BUILDS):
129+
running_builds = (
129130
Build.objects
130131
.filter(
131132
project=project,
132133
version=version,
133-
commit=commit,
134134
).exclude(
135135
state__in=BUILD_FINAL_STATES,
136136
).exclude(
137137
pk=build.pk,
138-
).exists()
138+
)
139139
)
140+
if running_builds.count() > 0:
141+
log.warning(
142+
"Canceling running builds automatically due a new one arrived.",
143+
running_builds=running_builds.count(),
144+
)
145+
146+
# If there are builds triggered/running for this particular project and version,
147+
# we cancel all of them and trigger a new one for the latest commit received.
148+
for running_build in running_builds:
149+
cancel_build(running_build)
140150
else:
141-
skip_build = Build.objects.filter(
142-
project=project,
143-
version=version,
144-
state=BUILD_STATE_TRIGGERED,
145-
# By filtering for builds triggered in the previous 5 minutes we
146-
# avoid false positives for builds that failed for any reason and
147-
# didn't update their state, ending up on blocked builds for that
148-
# version (all the builds are marked as DUPLICATED in that case).
149-
# Adding this date condition, we reduce the risk of hitting this
150-
# problem to 5 minutes only.
151-
date__gte=timezone.now() - datetime.timedelta(minutes=5),
152-
).count() > 1
153-
154-
if not project.has_feature(Feature.DEDUPLICATE_BUILDS):
155-
log.debug(
156-
'Skipping deduplication of builds. Feature not enabled.',
157-
project_slug=project.slug,
158-
)
159-
skip_build = False
151+
# NOTE: de-duplicate builds won't be required if we enable `CANCEL_OLD_BUILDS`,
152+
# since canceling a build is more effective.
153+
# However, we are keepting `DEDUPLICATE_BUILDS` code around while we test
154+
# `CANCEL_OLD_BUILDS` with a few projects and we are happy with the results.
155+
# After that, we can remove `DEDUPLICATE_BUILDS` code
156+
# and make `CANCEL_OLD_BUILDS` the default behavior.
157+
if commit:
158+
skip_build = (
159+
Build.objects.filter(
160+
project=project,
161+
version=version,
162+
commit=commit,
163+
)
164+
.exclude(
165+
state__in=BUILD_FINAL_STATES,
166+
)
167+
.exclude(
168+
pk=build.pk,
169+
)
170+
.exists()
171+
)
172+
else:
173+
skip_build = (
174+
Build.objects.filter(
175+
project=project,
176+
version=version,
177+
state=BUILD_STATE_TRIGGERED,
178+
# By filtering for builds triggered in the previous 5 minutes we
179+
# avoid false positives for builds that failed for any reason and
180+
# didn't update their state, ending up on blocked builds for that
181+
# version (all the builds are marked as DUPLICATED in that case).
182+
# Adding this date condition, we reduce the risk of hitting this
183+
# problem to 5 minutes only.
184+
date__gte=timezone.now() - datetime.timedelta(minutes=5),
185+
).count()
186+
> 1
187+
)
160188

161-
if skip_build:
162-
# TODO: we could mark the old build as duplicated, however we reset our
163-
# position in the queue and go back to the end of it --penalization
164-
log.warning(
165-
'Marking build to be skipped by builder.',
166-
project_slug=project.slug,
167-
version_slug=version.slug,
168-
build_id=build.pk,
169-
commit=commit,
170-
)
171-
build.error = DuplicatedBuildError.message
172-
build.status = DuplicatedBuildError.status
173-
build.exit_code = DuplicatedBuildError.exit_code
174-
build.success = False
175-
build.state = BUILD_STATE_CANCELLED
176-
build.save()
189+
if not project.has_feature(Feature.DEDUPLICATE_BUILDS):
190+
log.debug(
191+
"Skipping deduplication of builds. Feature not enabled.",
192+
)
193+
skip_build = False
194+
195+
if skip_build:
196+
# TODO: we could mark the old build as duplicated, however we reset our
197+
# position in the queue and go back to the end of it --penalization
198+
log.warning(
199+
"Marking build to be skipped by builder.",
200+
)
201+
build.error = DuplicatedBuildError.message
202+
build.status = DuplicatedBuildError.status
203+
build.exit_code = DuplicatedBuildError.exit_code
204+
build.success = False
205+
build.state = BUILD_STATE_CANCELLED
206+
build.save()
177207

178208
# Start the build in X minutes and mark it as limited
179209
if not skip_build and project.has_feature(Feature.LIMIT_CONCURRENT_BUILDS):

readthedocs/projects/models.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -1859,12 +1859,13 @@ def add_features(sender, **kwargs):
18591859
DEFAULT_TO_FUZZY_SEARCH = 'default_to_fuzzy_search'
18601860
INDEX_FROM_HTML_FILES = 'index_from_html_files'
18611861

1862-
LIST_PACKAGES_INSTALLED_ENV = 'list_packages_installed_env'
1863-
VCS_REMOTE_LISTING = 'vcs_remote_listing'
1864-
SPHINX_PARALLEL = 'sphinx_parallel'
1865-
USE_SPHINX_BUILDERS = 'use_sphinx_builders'
1866-
DEDUPLICATE_BUILDS = 'deduplicate_builds'
1867-
DONT_CREATE_INDEX = 'dont_create_index'
1862+
LIST_PACKAGES_INSTALLED_ENV = "list_packages_installed_env"
1863+
VCS_REMOTE_LISTING = "vcs_remote_listing"
1864+
SPHINX_PARALLEL = "sphinx_parallel"
1865+
USE_SPHINX_BUILDERS = "use_sphinx_builders"
1866+
DEDUPLICATE_BUILDS = "deduplicate_builds"
1867+
CANCEL_OLD_BUILDS = "cancel_old_builds"
1868+
DONT_CREATE_INDEX = "dont_create_index"
18681869

18691870
FEATURES = (
18701871
(ALLOW_DEPRECATED_WEBHOOKS, _('Allow deprecated webhook views')),
@@ -2015,6 +2016,12 @@ def add_features(sender, **kwargs):
20152016
DEDUPLICATE_BUILDS,
20162017
_('Mark duplicated builds as NOOP to be skipped by builders'),
20172018
),
2019+
(
2020+
CANCEL_OLD_BUILDS,
2021+
_(
2022+
"Cancel triggered/running builds when a new one for the same project/version arrives"
2023+
),
2024+
),
20182025
(
20192026
DONT_CREATE_INDEX,
20202027
_('Do not create index.md or README.rst if the project does not have one.'),

0 commit comments

Comments
 (0)