Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit concurrent builds #6847

Merged
merged 7 commits into from
Apr 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion readthedocs/api/v2/views/model_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
from rest_framework.renderers import BaseRenderer, JSONRenderer
from rest_framework.response import Response

from readthedocs.builds.constants import BRANCH, TAG, INTERNAL
from readthedocs.builds.constants import (
BRANCH,
TAG,
INTERNAL,
BUILD_STATE_FINISHED,
)
from readthedocs.builds.models import Build, BuildCommandResult, Version
from readthedocs.core.utils import trigger_build
from readthedocs.core.utils.extend import SettingsOverrideObject
Expand Down Expand Up @@ -276,6 +281,20 @@ class BuildViewSetBase(UserSelectViewSet):
model = Build
filterset_fields = ('project__slug', 'commit')

@decorators.action(
detail=False,
permission_classes=[permissions.IsAdminUser],
methods=['get'],
)
def running(self, request, **kwargs):
project_slug = request.GET.get('project__slug')
queryset = (
self.get_queryset()
.filter(project__slug=project_slug)
.exclude(state__in=[BUILD_STATE_FINISHED])
)
return Response({'count': queryset.count()})


class BuildViewSet(SettingsOverrideObject):

Expand Down
4 changes: 4 additions & 0 deletions readthedocs/doc_builder/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class BuildTimeoutError(BuildEnvironmentError):
message = ugettext_noop('Build exited due to time out')


class BuildMaxConcurrencyError(BuildEnvironmentError):
message = ugettext_noop('Concurrency limit reached ({limit}), retrying in 5 minutes.')


class BuildEnvironmentWarning(BuildEnvironmentException):
pass

Expand Down
5 changes: 5 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,7 @@ def add_features(sender, **kwargs):
SKIP_SYNC_TAGS = 'skip_sync_tags'
SKIP_SYNC_BRANCHES = 'skip_sync_branches'
CACHED_ENVIRONMENT = 'cached_environment'
LIMIT_CONCURRENT_BUILDS = 'limit_concurrent_builds'

FEATURES = (
(USE_SPHINX_LATEST, _('Use latest version of Sphinx')),
Expand Down Expand Up @@ -1585,6 +1586,10 @@ def add_features(sender, **kwargs):
CACHED_ENVIRONMENT,
_('Cache the environment (virtualenv, conda, pip cache, repository) in storage'),
),
(
LIMIT_CONCURRENT_BUILDS,
_('Limit the amount of concurrent builds'),
),
)

projects = models.ManyToManyField(
Expand Down
25 changes: 25 additions & 0 deletions readthedocs/projects/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from readthedocs.doc_builder.exceptions import (
BuildEnvironmentError,
BuildEnvironmentWarning,
BuildMaxConcurrencyError,
BuildTimeoutError,
MkDocsYAMLParseError,
ProjectBuildsSkippedError,
Expand Down Expand Up @@ -510,6 +511,30 @@ def run(
self.commit = commit
self.config = None

if self.project.has_feature(Feature.LIMIT_CONCURRENT_BUILDS):
response = api_v2.build.running.get(project__slug=self.project.slug)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use APIv3 here (https://docs.readthedocs.io/en/stable/api/v3.html#builds-listing) if we add a new permission class to allow build user to list builds. As this will require extra work and we are only using APIv2 from builders, I didn't want to mix them here.

if response.get('count', 0) >= settings.RTD_MAX_CONCURRENT_BUILDS:
log.warning(
'Delaying tasks due to concurrency limit. project=%s version=%s',
self.project.slug,
self.version.slug,
)

# This is done automatically on the environment context, but
# we are executing this code before creating one
api_v2.build(self.build['id']).patch({
'error': BuildMaxConcurrencyError.message.format(
limit=settings.RTD_MAX_CONCURRENT_BUILDS,
),
})
self.task.retry(
exc=BuildMaxConcurrencyError,
throw=False,
# We want to retry this build more times
max_retries=25,
)
return False

# Build process starts here
setup_successful = self.run_setup(record=record)
if not setup_successful:
Expand Down
1 change: 1 addition & 0 deletions readthedocs/rtd_tests/tests/test_privacy_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ def setUp(self):
'api_webhook': {'integration_pk': self.integration.pk},
}
self.response_data = {
'build-running': {'status_code': 403},
'project-sync-versions': {'status_code': 403},
'project-token': {'status_code': 403},
'emailhook-list': {'status_code': 403},
Expand Down
1 change: 1 addition & 0 deletions readthedocs/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class CommunityBaseSettings(Settings):
RTD_STABLE = 'stable'
RTD_STABLE_VERBOSE_NAME = 'stable'
RTD_CLEAN_AFTER_BUILD = False
RTD_MAX_CONCURRENT_BUILDS = 4

# Database and API hitting settings
DONT_HIT_API = False
Expand Down