diff --git a/binderhub/app.py b/binderhub/app.py index e64e021cce..d4ca8719a4 100755 --- a/binderhub/app.py +++ b/binderhub/app.py @@ -57,7 +57,7 @@ DataverseProvider) from .metrics import MetricsHandler -from .utils import ByteSpecification, url_path_join +from .utils import CPUSpecification, ByteSpecification, url_path_join from .events import EventLog @@ -335,6 +335,26 @@ def _valid_badge_base_url(self, proposal): config=True ) + build_cpu_request = CPUSpecification( + 0, + help=""" + Amount of cpu to request when scheduling a build + + 0 reserves no cpu. + + """, + config=True, + ) + build_cpu_limit = CPUSpecification( + 0, + help=""" + Max amount of cpu allocated for each image build process. + + 0 sets no limit. + """, + config=True, + ) + build_memory_request = ByteSpecification( 0, help=""" @@ -773,6 +793,8 @@ def initialize(self, *args, **kwargs): "jinja2_env": jinja_env, "build_memory_limit": self.build_memory_limit, "build_memory_request": self.build_memory_request, + "build_cpu_limit": self.build_cpu_limit, + "build_cpu_request": self.build_cpu_request, "build_docker_host": self.build_docker_host, "build_docker_config": self.build_docker_config, "base_url": self.base_url, diff --git a/binderhub/build.py b/binderhub/build.py index 2ec263e23e..f130797f9f 100644 --- a/binderhub/build.py +++ b/binderhub/build.py @@ -51,6 +51,8 @@ def __init__( image_name, git_credentials=None, push_secret=None, + cpu_limit=0, + cpu_request=0, memory_limit=0, memory_request=0, node_selector=None, @@ -95,6 +97,18 @@ def __init__( https://git-scm.com/docs/gitcredentials for more information. push_secret : str Kubernetes secret containing credentials to push docker image to registry. + cpu_limit + CPU limit for the docker build process. Can be an integer (1), fraction (0.5) or + millicore specification (100m). Value should adhere to K8s specification + for CPU meaning. See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu + for more information + cpu_request + CPU request of the build pod. The actual building happens in the + docker daemon, but setting request in the build pod makes sure that + cpu is reserved for the docker build in the node by the kubernetes + scheduler. Value should adhere to K8s specification for CPU meaning. + See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu + for more information memory_limit Memory limit for the docker build process. Can be an integer in bytes, or a byte specification (like 6M). @@ -129,6 +143,8 @@ def __init__( self.push_secret = push_secret self.build_image = build_image self.main_loop = IOLoop.current() + self.cpu_limit = cpu_limit + self.cpu_request = cpu_request self.memory_limit = memory_limit self.memory_request = memory_request self.docker_host = docker_host @@ -343,8 +359,8 @@ def submit(self): args=self.get_cmd(), volume_mounts=volume_mounts, resources=client.V1ResourceRequirements( - limits={'memory': self.memory_limit}, - requests={'memory': self.memory_request}, + limits={'memory': self.memory_limit, 'cpu': self.cpu_limit}, + requests={'memory': self.memory_request, 'cpu': self.cpu_request}, ), env=env ) diff --git a/binderhub/builder.py b/binderhub/builder.py index ca41c2270d..ca683a4045 100644 --- a/binderhub/builder.py +++ b/binderhub/builder.py @@ -411,6 +411,8 @@ async def get(self, provider_prefix, _unescaped_spec): image_name=image_name, push_secret=push_secret, build_image=self.settings['build_image'], + cpu_limit=self.settings['build_cpu_limit'], + cpu_request=self.settings['build_cpu_request'], memory_limit=self.settings['build_memory_limit'], memory_request=self.settings['build_memory_request'], docker_host=self.settings['build_docker_host'], diff --git a/binderhub/utils.py b/binderhub/utils.py index 4bf041adf0..187a5ae564 100644 --- a/binderhub/utils.py +++ b/binderhub/utils.py @@ -4,7 +4,7 @@ import ipaddress import time -from traitlets import Integer, TraitError +from traitlets import Unicode, Integer, TraitError # default _request_timeout for kubernetes api requests @@ -42,6 +42,74 @@ def rendezvous_rank(buckets, key): return [b for (s, b) in sorted(ranking, reverse=True)] +class CPUSpecification(Unicode): + """ + Allows specifying CPU limits + + Suffixes allowed are: + - m -> millicore + + """ + + # Default to allowing None as a value + allow_none = True + + def validate(self, obj, value): + """ + Validate that the passed in value is a valid cpu specification + in the K8s CPU meaning. + + See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu + + It could either be a pure int or float, when it is taken as a value. + In case of integer it can optionally have 'm' suffix to designate millicores. + """ + + if isinstance(value, int): + return int(value) + + if isinstance(value, float): + return float(value) + + # Try treat it as integer + _int_value = None + try: + _int_value = int(value) + except ValueError: + pass + + if isinstance(_int_value, int): + return _int_value + + # Try treat it as float + _float_value = None + try: + _float_value = float(value) + except ValueError: + pass + + if isinstance(_float_value, float): + return _float_value + + # Try treat it as millicore spec + try: + _unused = int(value[:-1]) + except ValueError: + raise TraitError( + "{val} is not a valid cpu specification".format( + val=value + ) + ) + + if value[-1] not in ['m']: + raise TraitError( + "{val} is not a valid cpu specification".format( + val=value + ) + ) + + return value + class ByteSpecification(Integer): """ Allow easily specifying bytes in units of 1024 with suffixes