Skip to content

Limit concurrency per-organization #7489

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

Merged
merged 3 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 17 additions & 2 deletions readthedocs/builds/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ def concurrent(self, project):

- translation: concurrent builds of all the translations + builds of main project

.. note::

If the project/translation belongs to an organization, we count all concurrent
builds for all the projects from the organization.

:rtype: tuple
:returns: limit_reached, number of concurrent builds, number of max concurrent
"""
Expand All @@ -149,12 +154,22 @@ def concurrent(self, project):
# The project has translations, counts their builds as well
query |= Q(project__in=project.translations.all())

# If the project belongs to an organization, count all the projects
# from this organization as well
organization = project.organizations.first()
if organization:
query |= Q(project__in=organization.projects.all())

concurrent = (
self.filter(query)
.exclude(state__in=[BUILD_STATE_TRIGGERED, BUILD_STATE_FINISHED])
).count()
).distinct().count()

max_concurrent = project.max_concurrent_builds or settings.RTD_MAX_CONCURRENT_BUILDS
max_concurrent = (
project.max_concurrent_builds or
(organization.max_concurrent_builds if organization else 0) or
Copy link
Member

Choose a reason for hiding this comment

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

Do we want else 0 here? Will the 0 cause the or to move to the default? It seems a bit confusing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this may be a little tricky. Basically the 0 here works as False and makes the or to continue to the next value:

In [27]: None or 0 or 5
Out[27]: 5

settings.RTD_MAX_CONCURRENT_BUILDS
)
log.info(
'Concurrent builds. project=%s running=%s max=%s',
project.slug,
Expand Down
86 changes: 86 additions & 0 deletions readthedocs/builds/tests/test_build_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from readthedocs.builds.querysets import BuildQuerySet
from readthedocs.builds.models import Build, Version
from readthedocs.organizations.models import Organization
from readthedocs.projects.models import Project, Feature


Expand Down Expand Up @@ -73,3 +74,88 @@ def test_concurrent_builds_translations(self):
)
assert (True, 4, 4) == Build.objects.concurrent(translation)
assert (True, 4, 4) == Build.objects.concurrent(project)

def test_concurrent_builds_organization(self):
organization = fixture.get(
Organization,
max_concurrent_builds=None,
)

for _ in range(2):
project = fixture.get(
Project,
max_concurrent_builds=None,
main_language_project=None,
)
organization.projects.add(project)

for project in organization.projects.all():
for state in ('triggered', 'building', 'cloning', 'finished'):
fixture.get(
Build,
project=project,
state=state,
)

project = organization.projects.first()
assert (True, 4, 4) == Build.objects.concurrent(project)
for state in ('building', 'cloning'):
fixture.get(
Build,
project=project,
state=state,
)
assert (True, 6, 4) == Build.objects.concurrent(project)

def test_concurrent_builds_organization_limited(self):
organization = fixture.get(
Organization,
max_concurrent_builds=10,
)
project_with_builds = fixture.get(
Project,
max_concurrent_builds=None,
main_language_project=None,
)
project_without_builds = fixture.get(
Project,
max_concurrent_builds=None,
main_language_project=None,
)
organization.projects.add(project_with_builds)
organization.projects.add(project_without_builds)
for state in ('triggered', 'building', 'cloning', 'finished'):
fixture.get(
Build,
project=project_with_builds,
state=state,
)
# Calling it with ``project_without_builds`` should count the builds
# from ``project_with_builds`` as well
assert (False, 2, 10) == Build.objects.concurrent(project_without_builds)

def test_concurrent_builds_organization_and_project_limited(self):
organization = fixture.get(
Organization,
max_concurrent_builds=10,
)
project_limited = fixture.get(
Project,
max_concurrent_builds=2,
main_language_project=None,
)
project_not_limited = fixture.get(
Project,
max_concurrent_builds=None,
main_language_project=None,
)
organization.projects.add(project_limited)
organization.projects.add(project_not_limited)
for state in ('triggered', 'building', 'cloning', 'finished'):
fixture.get(
Build,
project=project_limited,
state=state,
)
assert (True, 2, 2) == Build.objects.concurrent(project_limited)
assert (False, 2, 10) == Build.objects.concurrent(project_not_limited)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.16 on 2020-09-17 09:54

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('organizations', '0003_team_auto_join_email_users'),
]

operations = [
migrations.AddField(
model_name='organization',
name='max_concurrent_builds',
field=models.IntegerField(blank=True, null=True, verbose_name='Maximum concurrent builds allowed for this organization'),
),
]
5 changes: 5 additions & 0 deletions readthedocs/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ class Organization(models.Model):
help_text='Docs and builds are disabled for this organization',
default=False,
)
max_concurrent_builds = models.IntegerField(
_('Maximum concurrent builds allowed for this organization'),
null=True,
blank=True,
)

stripe_id = models.CharField(
_('Stripe customer ID'),
Expand Down