Skip to content

Commit 95779a0

Browse files
authored
Build: use only one setting for build time limit (#12369)
Closes #12347 <!-- readthedocs-preview docs start --> --- :books: Documentation previews :books: - User's documentation (`docs`): https://docs--12369.org.readthedocs.build/12369/ <!-- readthedocs-preview docs end --> <!-- readthedocs-preview dev start --> - Developer's documentation (`dev`): https://dev--12369.org.readthedocs.build/12369/ <!-- readthedocs-preview dev end -->
1 parent 6697275 commit 95779a0

File tree

10 files changed

+61
-89
lines changed

10 files changed

+61
-89
lines changed

docs/dev/settings.rst

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
Interesting settings
22
====================
33

4-
DOCKER_LIMITS
5-
-------------
4+
BUILD_MEMORY_LIMIT
5+
------------------
66

7-
A dictionary of limits to virtual machines. These limits include:
7+
The maximum memory allocated to the virtual machine.
8+
If this limit is hit, build processes will be automatically killed.
9+
Examples: '200m' for 200MB of total memory, or '2g' for 2GB of total memory.
810

9-
time
10-
An integer representing the total allowed time limit (in
11-
seconds) of build processes. This time limit affects the parent
12-
process to the virtual machine and will force a virtual machine
13-
to die if a build is still running after the allotted time
14-
expires.
11+
BUILD_TIME_LIMIT
12+
----------------
1513

16-
memory
17-
The maximum memory allocated to the virtual machine. If this
18-
limit is hit, build processes will be automatically killed.
19-
Examples: '200m' for 200MB of total memory, or '2g' for 2GB of
20-
total memory.
14+
An integer representing the total allowed time limit (in seconds) of build processes.
15+
This time limit affects the parent process to the virtual machine and will force a virtual machine
16+
to die if a build is still running after the allotted time expires.
2117

2218
PRODUCTION_DOMAIN
2319
------------------

readthedocs/api/v2/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import timedelta
22

3+
from django.conf import settings
34
from django.db import models
45
from django.utils import timezone
56
from django.utils.translation import gettext_lazy as _
@@ -18,7 +19,9 @@ def create_key(self, project):
1819
Build API keys are valid for 3 hours,
1920
and can be revoked at any time by hitting the /api/v2/revoke/ endpoint.
2021
"""
21-
expiry_date = timezone.now() + timedelta(hours=3)
22+
# Use the project or default build time limit + 25% for the API token
23+
delta = (project.container_time_limit or settings.BUILD_TIME_LIMIT) * 1.25
24+
expiry_date = timezone.now() + timedelta(seconds=delta)
2225
name_max_length = self.model._meta.get_field("name").max_length
2326
return super().create_key(
2427
# Name is required, so we use the project slug for it.

readthedocs/core/utils/__init__.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,7 @@ def prepare_build(
8686
options["queue"] = project.build_queue
8787

8888
# Set per-task time limit
89-
# TODO remove the use of Docker limits or replace the logic here. This
90-
# was pulling the Docker limits that were set on each stack, but we moved
91-
# to dynamic setting of the Docker limits. This sets a failsafe higher
92-
# limit, but if no builds hit this limit, it should be safe to remove and
93-
# rely on Docker to terminate things on time.
94-
# time_limit = DOCKER_LIMITS['time']
95-
time_limit = 7200
96-
try:
97-
if project.container_time_limit:
98-
time_limit = int(project.container_time_limit)
99-
except ValueError:
100-
log.warning("Invalid time_limit for project.")
89+
time_limit = project.container_time_limit or settings.BUILD_TIME_LIMIT
10190

10291
# Add 20% overhead to task, to ensure the build can timeout and the task
10392
# will cleanly finish.

readthedocs/doc_builder/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
DOCKER_SOCKET = settings.DOCKER_SOCKET
1515
DOCKER_VERSION = settings.DOCKER_VERSION
1616
DOCKER_IMAGE = settings.DOCKER_IMAGE
17-
DOCKER_LIMITS = settings.DOCKER_LIMITS
1817
DOCKER_TIMEOUT_EXIT_CODE = 42
1918
DOCKER_OOM_EXIT_CODE = 137
2019

readthedocs/doc_builder/environments.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
from .constants import DOCKER_HOSTNAME_MAX_LEN
2727
from .constants import DOCKER_IMAGE
28-
from .constants import DOCKER_LIMITS
2928
from .constants import DOCKER_OOM_EXIT_CODE
3029
from .constants import DOCKER_SOCKET
3130
from .constants import DOCKER_TIMEOUT_EXIT_CODE
@@ -594,8 +593,6 @@ class DockerBuildEnvironment(BaseBuildEnvironment):
594593

595594
command_class = DockerBuildCommand
596595
container_image = DOCKER_IMAGE
597-
container_mem_limit = DOCKER_LIMITS.get("memory")
598-
container_time_limit = DOCKER_LIMITS.get("time")
599596

600597
def __init__(self, *args, **kwargs):
601598
container_image = kwargs.pop("container_image", None)
@@ -622,10 +619,8 @@ def __init__(self, *args, **kwargs):
622619
if container_image:
623620
self.container_image = container_image
624621

625-
if self.project.container_mem_limit:
626-
self.container_mem_limit = self.project.container_mem_limit
627-
if self.project.container_time_limit:
628-
self.container_time_limit = self.project.container_time_limit
622+
self.container_mem_limit = self.project.container_mem_limit or settings.BUILD_MEMORY_LIMIT
623+
self.container_time_limit = self.project.container_time_limit or settings.BUILD_TIME_LIMIT
629624

630625
structlog.contextvars.bind_contextvars(
631626
project_slug=self.project.slug,

readthedocs/projects/tasks/utils.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,13 @@ def finish_inactive_builds():
162162
163163
A build is consider inactive if it's not in a final state and it has been
164164
"running" for more time that the allowed one (``Project.container_time_limit``
165-
or ``DOCKER_LIMITS['time']`` plus a 20% of it).
165+
or ``BUILD_TIME_LIMIT`` plus a 20% of it).
166166
167167
These inactive builds will be marked as ``success`` and ``CANCELLED`` with an
168168
``error`` to be communicated to the user.
169169
"""
170-
# TODO similar to the celery task time limit, we can't infer this from
171-
# Docker settings anymore, because Docker settings are determined on the
172-
# build servers dynamically.
173-
# time_limit = int(DOCKER_LIMITS['time'] * 1.2)
174-
# Set time as maximum celery task time limit + 5m
175-
time_limit = 7200 + 300
170+
# TODO: delete this task once we are fully migrated to ``BUILD_HEALTHCHECK``
171+
time_limit = settings.BUILD_TIME_LIMIT * 1.2
176172
delta = datetime.timedelta(seconds=time_limit)
177173
query = (
178174
~Q(state__in=BUILD_FINAL_STATES)

readthedocs/rtd_tests/tests/test_core_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest import mock
33

44
import pytest
5+
from django.conf import settings
56
from django.test import TestCase, override_settings
67
from django_dynamic_fixture import get
78

@@ -189,8 +190,8 @@ def test_trigger_max_concurrency_reached(self, update_docs, app):
189190
trigger_build(project=self.project, version=self.version)
190191
kwargs = {"build_commit": None, "build_api_key": mock.ANY}
191192
options = {
192-
"time_limit": int(7200 * 1.2),
193-
"soft_time_limit": 7200,
193+
"time_limit": settings.BUILD_TIME_LIMIT * 1.2,
194+
"soft_time_limit": settings.BUILD_TIME_LIMIT,
194195
"countdown": 5 * 60,
195196
"max_retries": 25,
196197
}

readthedocs/settings/base.py

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,33 @@ def TEMPLATES(self):
589589
USE_I18N = True
590590
USE_L10N = True
591591

592+
BUILD_TIME_LIMIT = 900 # seconds
593+
594+
@property
595+
def BUILD_MEMORY_LIMIT(self):
596+
"""
597+
Set build memory limit dynamically, if in production, based on system memory.
598+
599+
We do this to avoid having separate build images. This assumes 1 build
600+
process per server, which will be allowed to consume all available
601+
memory.
602+
"""
603+
# Our normal default
604+
default_memory_limit = "7g"
605+
606+
# Only run on our servers
607+
if self.RTD_IS_PRODUCTION:
608+
total_memory, memory_limit = self._get_build_memory_limit()
609+
610+
memory_limit = memory_limit or default_memory_limit
611+
log.info(
612+
"Using dynamic build limits.",
613+
hostname=socket.gethostname(),
614+
memory=memory_limit,
615+
)
616+
return memory_limit
617+
618+
592619
# Celery
593620
CELERY_APP_NAME = "readthedocs"
594621
CELERY_ALWAYS_EAGER = True
@@ -605,7 +632,7 @@ def TEMPLATES(self):
605632
# https://github.com/readthedocs/readthedocs.org/issues/12317#issuecomment-3070950434
606633
# https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/redis.html#visibility-timeout
607634
BROKER_TRANSPORT_OPTIONS = {
608-
'visibility_timeout': 18000, # 5 hours
635+
'visibility_timeout': BUILD_TIME_LIMIT * 1.15, # 15% more than the build time limit
609636
}
610637

611638
CELERY_DEFAULT_QUEUE = "celery"
@@ -721,7 +748,13 @@ def TEMPLATES(self):
721748
# since we can't read their config file image choice before cloning
722749
RTD_DOCKER_CLONE_IMAGE = RTD_DOCKER_BUILD_SETTINGS["os"]["ubuntu-22.04"]
723750

724-
def _get_docker_memory_limit(self):
751+
def _get_build_memory_limit(self):
752+
"""
753+
Return the buld memory limit based on available system memory.
754+
755+
We subtract ~1000Mb for overhead of processes and base system, and set
756+
the build time as proportional to the memory limit.
757+
"""
725758
try:
726759
total_memory = int(
727760
subprocess.check_output(
@@ -735,47 +768,6 @@ def _get_docker_memory_limit(self):
735768
# int and raise a ValueError
736769
log.exception("Failed to get memory size, using defaults Docker limits.")
737770

738-
# Coefficient used to determine build time limit, as a percentage of total
739-
# memory. Historical values here were 0.225 to 0.3.
740-
DOCKER_TIME_LIMIT_COEFF = 0.25
741-
742-
@property
743-
def DOCKER_LIMITS(self):
744-
"""
745-
Set docker limits dynamically, if in production, based on system memory.
746-
747-
We do this to avoid having separate build images. This assumes 1 build
748-
process per server, which will be allowed to consume all available
749-
memory.
750-
751-
We subtract 750MiB for overhead of processes and base system, and set
752-
the build time as proportional to the memory limit.
753-
"""
754-
# Our normal default
755-
limits = {
756-
"memory": "2g",
757-
"time": 900,
758-
}
759-
760-
# Only run on our servers
761-
if self.RTD_IS_PRODUCTION:
762-
total_memory, memory_limit = self._get_docker_memory_limit()
763-
if memory_limit:
764-
limits = {
765-
"memory": f"{memory_limit}m",
766-
"time": max(
767-
limits["time"],
768-
round(total_memory * self.DOCKER_TIME_LIMIT_COEFF, -2),
769-
),
770-
}
771-
log.info(
772-
"Using dynamic docker limits.",
773-
hostname=socket.gethostname(),
774-
memory=limits["memory"],
775-
time=limits["time"],
776-
)
777-
return limits
778-
779771
# Allauth
780772
ACCOUNT_ADAPTER = "readthedocs.core.adapters.AccountAdapter"
781773
SOCIALACCOUNT_ADAPTER = 'readthedocs.core.adapters.SocialAccountAdapter'

readthedocs/settings/docker_compose.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class DockerBaseSettings(CommunityBaseSettings):
1515
RTD_DOCKER_COMPOSE_NETWORK = "community_readthedocs"
1616
RTD_DOCKER_COMPOSE_VOLUME = "community_build-user-builds"
1717
RTD_DOCKER_USER = f"{os.geteuid()}:{os.getegid()}"
18-
DOCKER_LIMITS = {"memory": "2g", "time": 900}
18+
BUILD_MEMORY_LIMIT = "2g"
1919

2020
PRODUCTION_DOMAIN = os.environ.get("RTD_PRODUCTION_DOMAIN", "devthedocs.org")
2121
PUBLIC_DOMAIN = os.environ.get("RTD_PUBLIC_DOMAIN", "devthedocs.org")

readthedocs/settings/test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class CommunityTestSettings(CommunityBaseSettings):
2626
CELERY_ALWAYS_EAGER = True
2727

2828
# Skip automatic detection of Docker limits for testing
29-
DOCKER_LIMITS = {"memory": "200m", "time": 600}
29+
BUILD_TIME_LIMIT = 600
30+
BUILD_MEMORY_LIMIT = "200m"
3031

3132
CACHES = {
3233
"default": {

0 commit comments

Comments
 (0)