Skip to content

Commit 4a511f1

Browse files
committed
CPU requests and limits for build pod
1 parent 5ba7fe1 commit 4a511f1

File tree

5 files changed

+167
-4
lines changed

5 files changed

+167
-4
lines changed

binderhub/app.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
DataverseProvider)
5858
from .metrics import MetricsHandler
5959

60-
from .utils import ByteSpecification, url_path_join
60+
from .utils import CPUSpecification, ByteSpecification, url_path_join
6161
from .events import EventLog
6262

6363

@@ -335,6 +335,26 @@ def _valid_badge_base_url(self, proposal):
335335
config=True
336336
)
337337

338+
build_cpu_request = CPUSpecification(
339+
0,
340+
help="""
341+
Amount of cpu to request when scheduling a build
342+
343+
0 reserves no cpu.
344+
345+
""",
346+
config=True,
347+
)
348+
build_cpu_limit = CPUSpecification(
349+
0,
350+
help="""
351+
Max amount of cpu allocated for each image build process.
352+
353+
0 sets no limit.
354+
""",
355+
config=True,
356+
)
357+
338358
build_memory_request = ByteSpecification(
339359
0,
340360
help="""
@@ -773,6 +793,8 @@ def initialize(self, *args, **kwargs):
773793
"jinja2_env": jinja_env,
774794
"build_memory_limit": self.build_memory_limit,
775795
"build_memory_request": self.build_memory_request,
796+
"build_cpu_limit": self.build_cpu_limit,
797+
"build_cpu_request": self.build_cpu_request,
776798
"build_docker_host": self.build_docker_host,
777799
"build_docker_config": self.build_docker_config,
778800
"base_url": self.base_url,

binderhub/build.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def __init__(
5151
image_name,
5252
git_credentials=None,
5353
push_secret=None,
54+
cpu_limit=0,
55+
cpu_request=0,
5456
memory_limit=0,
5557
memory_request=0,
5658
node_selector=None,
@@ -95,6 +97,18 @@ def __init__(
9597
https://git-scm.com/docs/gitcredentials for more information.
9698
push_secret : str
9799
Kubernetes secret containing credentials to push docker image to registry.
100+
cpu_limit
101+
CPU limit for the docker build process. Can be an integer (1), fraction (0.5) or
102+
millicore specification (100m). Value should adhere to K8s specification
103+
for CPU meaning. See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu
104+
for more information
105+
cpu_request
106+
CPU request of the build pod. The actual building happens in the
107+
docker daemon, but setting request in the build pod makes sure that
108+
cpu is reserved for the docker build in the node by the kubernetes
109+
scheduler. Value should adhere to K8s specification for CPU meaning.
110+
See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu
111+
for more information
98112
memory_limit
99113
Memory limit for the docker build process. Can be an integer in
100114
bytes, or a byte specification (like 6M).
@@ -129,6 +143,8 @@ def __init__(
129143
self.push_secret = push_secret
130144
self.build_image = build_image
131145
self.main_loop = IOLoop.current()
146+
self.cpu_limit = cpu_limit
147+
self.cpu_request = cpu_request
132148
self.memory_limit = memory_limit
133149
self.memory_request = memory_request
134150
self.docker_host = docker_host
@@ -343,8 +359,8 @@ def submit(self):
343359
args=self.get_cmd(),
344360
volume_mounts=volume_mounts,
345361
resources=client.V1ResourceRequirements(
346-
limits={'memory': self.memory_limit},
347-
requests={'memory': self.memory_request},
362+
limits={'memory': self.memory_limit, 'cpu': self.cpu_limit},
363+
requests={'memory': self.memory_request, 'cpu': self.cpu_request},
348364
),
349365
env=env
350366
)

binderhub/builder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ async def get(self, provider_prefix, _unescaped_spec):
411411
image_name=image_name,
412412
push_secret=push_secret,
413413
build_image=self.settings['build_image'],
414+
cpu_limit=self.settings['build_cpu_limit'],
415+
cpu_request=self.settings['build_cpu_request'],
414416
memory_limit=self.settings['build_memory_limit'],
415417
memory_request=self.settings['build_memory_request'],
416418
docker_host=self.settings['build_docker_host'],

binderhub/tests/test_utils.py

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

44
import pytest
5+
from traitlets.traitlets import TraitError
56

67
from binderhub import utils
78

@@ -137,3 +138,45 @@ def test_ip_in_networks(ip, cidrs, found):
137138
def test_ip_in_networks_invalid():
138139
with pytest.raises(ValueError):
139140
utils.ip_in_networks("1.2.3.4", {}, 0)
141+
142+
@pytest.mark.parametrize(
143+
"value, coerced",
144+
[
145+
("2", 2),
146+
(2, 2),
147+
("0.2", 0.2),
148+
(0.2, 0.2),
149+
("200m", "200m"),
150+
(None, 0),
151+
(0, 0),
152+
("0", 0),
153+
]
154+
)
155+
def test_cpu_specification_valid(value, coerced):
156+
cpu_spec = utils.CPUSpecification()
157+
assert cpu_spec.validate(None, value) == coerced
158+
159+
@pytest.mark.parametrize(
160+
"value",
161+
[
162+
("0.2m"),
163+
("deadbeef"),
164+
("m"),
165+
(""),
166+
("200M"),
167+
("200k"),
168+
("-1"),
169+
("-1m"),
170+
("-0.1m"),
171+
(-0.1),
172+
(-1),
173+
(False),
174+
(True),
175+
([]),
176+
({}),
177+
]
178+
)
179+
def test_cpu_specification_invalid(value):
180+
cpu_spec = utils.CPUSpecification()
181+
with pytest.raises(TraitError):
182+
_ = cpu_spec.validate(None, value)

binderhub/utils.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import ipaddress
55
import time
66

7-
from traitlets import Integer, TraitError
7+
from traitlets import Unicode, Integer, TraitError
88

99

1010
# default _request_timeout for kubernetes api requests
@@ -42,6 +42,86 @@ def rendezvous_rank(buckets, key):
4242
return [b for (s, b) in sorted(ranking, reverse=True)]
4343

4444

45+
class CPUSpecification(Unicode):
46+
"""
47+
Allows specifying CPU limits
48+
49+
Suffixes allowed are:
50+
- m -> millicore
51+
52+
"""
53+
54+
# Default to allowing None as a value
55+
allow_none = True
56+
57+
def validate(self, obj, value):
58+
"""
59+
Validate that the passed in value is a valid cpu specification
60+
in the K8s CPU meaning.
61+
62+
See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu
63+
64+
It could either be a pure int or float, when it is taken as a value.
65+
In case of integer it can optionally have 'm' suffix to designate millicores.
66+
"""
67+
68+
def raise_error(value):
69+
raise TraitError(
70+
"{val} is not a valid cpu specification".format(
71+
val=value
72+
)
73+
)
74+
75+
# Positive filter for numberic values
76+
only_positive = lambda v : v if v >= 0 else raise_error(v)
77+
78+
if value is None:
79+
return 0
80+
81+
if isinstance(value, bool):
82+
raise_error(value)
83+
84+
if isinstance(value, int):
85+
return only_positive(int(value))
86+
87+
if isinstance(value, float):
88+
return only_positive(float(value))
89+
90+
# Must be string
91+
if not isinstance(value, str):
92+
raise_error(value)
93+
94+
# Try treat it as integer
95+
_int_value = None
96+
try:
97+
_int_value = int(value)
98+
except ValueError:
99+
pass
100+
101+
if isinstance(_int_value, int):
102+
return only_positive(_int_value)
103+
104+
# Try treat it as float
105+
_float_value = None
106+
try:
107+
_float_value = float(value)
108+
except ValueError:
109+
pass
110+
111+
if isinstance(_float_value, float):
112+
return only_positive(_float_value)
113+
114+
# Try treat it as millicore spec
115+
try:
116+
_unused = only_positive(int(value[:-1]))
117+
except ValueError:
118+
raise_error(value)
119+
120+
if value[-1] not in ['m']:
121+
raise_error(value)
122+
123+
return value
124+
45125
class ByteSpecification(Integer):
46126
"""
47127
Allow easily specifying bytes in units of 1024 with suffixes

0 commit comments

Comments
 (0)