Skip to content

Commit 9ef2c87

Browse files
authored
Resource limits now also from services key; Added tests (#1173)
* Resource limits now also from services key; Added tests * Test fixes [skip ci]
1 parent bfbddbc commit 9ef2c87

8 files changed

+301
-9
lines changed

lib/scenario_runner.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -947,14 +947,24 @@ def setup_services(self):
947947
if 'pause-after-phase' in service:
948948
self.__services_to_pause_phase[service['pause-after-phase']] = self.__services_to_pause_phase.get(service['pause-after-phase'], []) + [container_name]
949949

950-
if 'deploy' in service:
951-
if memory := service['deploy'].get('resources', {}).get('limits', {}).get('memory', None):
952-
docker_run_string.append('--memory') # value in bytes
953-
docker_run_string.append(str(memory))
954-
if cpus := service['deploy'].get('resources', {}).get('limits', {}).get('cpus', None):
955-
docker_run_string.append('--cpus') # value in cores
956-
docker_run_string.append(str(cpus))
957-
950+
# wildly the docker compose spec allows deploy to be None ... thus we need to check and cannot .get()
951+
if 'deploy' in service and service['deploy'] is not None and (memory := service['deploy'].get('resources', {}).get('limits', {}).get('memory', None)):
952+
docker_run_string.append('--memory') # value in bytes
953+
docker_run_string.append(str(memory))
954+
print('Applying Memory Limit from deploy')
955+
elif memory := service.get('mem_limit', None): # we only need to get resources or cpus. they must align anyway
956+
docker_run_string.append('--memory')
957+
docker_run_string.append(str(memory)) # value in bytes e.g. "10M"
958+
print('Applying Memory Limit from services')
959+
960+
if 'deploy' in service and service['deploy'] is not None and (cpus := service['deploy'].get('resources', {}).get('limits', {}).get('cpus', None)):
961+
docker_run_string.append('--cpus') # value in cores
962+
docker_run_string.append(str(cpus))
963+
print('Applying CPU Limit from deploy')
964+
elif cpus := service.get('cpus', None): # we only need to get resources or cpus. they must align anyway
965+
docker_run_string.append('--cpus')
966+
docker_run_string.append(str(cpus)) # value in (fractional) cores
967+
print('Applying CPU Limit from services')
958968

959969
if 'healthcheck' in service: # must come last
960970
if 'disable' in service['healthcheck'] and service['healthcheck']['disable'] is True:

lib/schema_checker.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,17 @@ def check_usage_scenario(self, usage_scenario):
105105
Optional("networks"): self.single_or_list(Use(self.contains_no_invalid_chars)),
106106
Optional("environment"): self.single_or_list(Or(dict,And(str, Use(self.not_empty)))),
107107
Optional("ports"): self.single_or_list(Or(And(str, Use(self.not_empty)), int)),
108-
Optional("depends_on"): Or([And(str, Use(self.not_empty))],dict),
108+
Optional('depends_on'): Or([And(str, Use(self.not_empty))],dict),
109+
Optional('deploy'):Or({
110+
Optional('resources'): {
111+
Optional('limits'): {
112+
Optional('cpus'): Or(str, float, int),
113+
Optional('memory') : str,
114+
}
115+
}
116+
}, None),
117+
Optional('mem_limit'): str,
118+
Optional('cpus') : Or(str, float, int),
109119
Optional('container_name'): And(str, Use(self.not_empty)),
110120
Optional("healthcheck"): {
111121
Optional('test'): Or(list, And(str, Use(self.not_empty))),
@@ -170,6 +180,8 @@ def check_usage_scenario(self, usage_scenario):
170180
raise SchemaError(error_message) from e
171181

172182

183+
184+
173185
# This check is necessary to do in a seperate pass. If tried to bake into the schema object above,
174186
# it will not know how to handle the value passed when it could be either a dict or list
175187
if 'networks' in usage_scenario:
@@ -191,6 +203,15 @@ def check_usage_scenario(self, usage_scenario):
191203
if 'cmd' in service:
192204
raise SchemaError(f"The 'cmd' key for service '{service_name}' is not supported anymore. Please migrate to 'command'")
193205

206+
207+
if (cpus := service.get('cpus')) and (cpus_deploy := service.get('deploy', {}).get('resources', {}).get('limits', {}).get('cpus')):
208+
if cpus != cpus_deploy:
209+
raise SchemaError('cpus service top level key and deploy.resources.limits.cpus must be identical')
210+
211+
if (mem_limit := service.get('mem_limit')) and (mem_limit_deploy := service.get('deploy', {}).get('resources', {}).get('limits', {}).get('memory')):
212+
if mem_limit != mem_limit_deploy:
213+
raise SchemaError('mem_limit service top level key and deploy.resources.limits.memory must be identical')
214+
194215
known_flow_names = []
195216
for flow in usage_scenario['flow']:
196217
if flow['name'] in known_flow_names:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Test Stress
3+
author: Dan Mateas
4+
description: test
5+
6+
services:
7+
8+
test-container-memory-int:
9+
type: container
10+
image: alpine
11+
deploy:
12+
resources:
13+
limits:
14+
cpus: 2
15+
cpus: 3
16+
17+
flow:
18+
- name: Stress
19+
container: test-container
20+
commands:
21+
- type: console
22+
command: stress-ng -c 1 -t 1 -q
23+
note: Starting Stress
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Test Stress
3+
author: Dan Mateas
4+
description: test
5+
6+
services:
7+
8+
test-container-memory-int:
9+
type: container
10+
image: alpine
11+
deploy:
12+
resources:
13+
limits:
14+
memory: "10M"
15+
mem_limit: "100M"
16+
17+
flow:
18+
- name: Stress
19+
container: test-container
20+
commands:
21+
- type: console
22+
command: stress-ng -c 1 -t 1 -q
23+
note: Starting Stress
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
name: Test Stress
3+
author: Dan Mateas
4+
description: test
5+
6+
services:
7+
test-container-only-cpu:
8+
type: container
9+
image: alpine
10+
deploy:
11+
resources:
12+
limits:
13+
cpus: "1.2"
14+
15+
test-container-only-memory:
16+
type: container
17+
image: alpine
18+
deploy:
19+
resources:
20+
limits:
21+
memory: "100MB"
22+
23+
test-container-both:
24+
type: container
25+
image: alpine
26+
deploy:
27+
resources:
28+
limits:
29+
cpus: "1.2"
30+
memory: "10M"
31+
32+
test-container-cpu-float:
33+
type: container
34+
image: alpine
35+
deploy:
36+
resources:
37+
limits:
38+
cpus: 1.2
39+
memory: "10M"
40+
41+
test-container-cpu-int:
42+
type: container
43+
image: alpine
44+
deploy:
45+
resources:
46+
limits:
47+
cpus: 1
48+
memory: "10M"
49+
50+
test-container-limits-partial:
51+
type: container
52+
image: alpine
53+
deploy: # allowed to be None
54+
55+
test-container-cpu-and-memory-in-both:
56+
type: container
57+
image: alpine
58+
deploy:
59+
resources:
60+
limits:
61+
cpus: 1
62+
memory: "10M"
63+
cpus: 1
64+
mem_limit: "10M"
65+
66+
test-container-limit-only-service-level:
67+
type: container
68+
image: alpine
69+
cpus: 1
70+
mem_limit: "10M"
71+
72+
73+
flow:
74+
- name: Stress
75+
container: test-container
76+
commands:
77+
- type: console
78+
command: stress-ng -c 1 -t 1 -q
79+
note: Starting Stress
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
name: Test Stress
3+
author: Dan Mateas
4+
description: test
5+
6+
services:
7+
8+
test-container-memory-int:
9+
type: container
10+
image: alpine
11+
deploy:
12+
resources:
13+
limits:
14+
memory: 1.2
15+
16+
flow:
17+
- name: Stress
18+
container: test-container
19+
commands:
20+
- type: console
21+
command: stress-ng -c 1 -t 1 -q
22+
note: Starting Stress
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Test Stress
3+
author: Dan Mateas
4+
description: test
5+
6+
services:
7+
8+
test-container-memory-int:
9+
type: container
10+
image: alpine
11+
deploy:
12+
resources:
13+
limits:
14+
memory: 1
15+
16+
17+
flow:
18+
- name: Stress
19+
container: test-container
20+
commands:
21+
- type: console
22+
command: stress-ng -c 1 -t 1 -q
23+
note: Starting Stress

tests/test_resource_limits.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# https://docs.docker.com/engine/reference/commandline/port/
2+
# List port mappings or a specific mapping for the container
3+
# docker port CONTAINER [PRIVATE_PORT[/PROTO]]
4+
5+
import io
6+
import os
7+
import subprocess
8+
9+
GMT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../')
10+
11+
from contextlib import redirect_stdout, redirect_stderr
12+
import pytest
13+
14+
from tests import test_functions as Tests
15+
from lib.scenario_runner import ScenarioRunner
16+
from lib.schema_checker import SchemaError
17+
18+
## Note:
19+
# Always do asserts after try:finally: blocks
20+
# otherwise failing Tests will not run the runner.cleanup() properly
21+
22+
23+
# This function runs the runner up to and *including* the specified step
24+
#pylint: disable=redefined-argument-from-local
25+
### The Tests for usage_scenario configurations
26+
27+
# environment: [object] (optional)
28+
# Key-Value pairs for ENV variables inside the container
29+
30+
def get_env_vars():
31+
ps = subprocess.run(
32+
['docker', 'exec', 'test-container', '/bin/sh',
33+
'-c', 'env'],
34+
check=True,
35+
stderr=subprocess.PIPE,
36+
stdout=subprocess.PIPE,
37+
encoding='UTF-8'
38+
)
39+
env_var_output = ps.stdout
40+
return env_var_output
41+
42+
def test_resource_limits_good():
43+
44+
runner = ScenarioRunner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/resource_limits_good.yml', skip_unsafe=False, skip_system_checks=True, dev_cache_build=True, dev_no_sleeps=True, dev_no_metrics=True, dev_no_phase_stats=True)
45+
46+
out = io.StringIO()
47+
err = io.StringIO()
48+
49+
with redirect_stdout(out), redirect_stderr(err):
50+
with Tests.RunUntilManager(runner) as context:
51+
context.run_until('setup_services')
52+
53+
assert err.getvalue() == ''
54+
assert 'Applying CPU Limit from deploy' in out.getvalue()
55+
assert 'Applying CPU Limit from services' in out.getvalue()
56+
assert 'Applying Memory Limit from deploy' in out.getvalue()
57+
assert 'Applying Memory Limit from services' in out.getvalue()
58+
59+
def test_resource_limits_memory_int():
60+
runner = ScenarioRunner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/resource_limits_memory_int.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
61+
with pytest.raises(SchemaError) as e:
62+
with Tests.RunUntilManager(runner) as context:
63+
context.run_until('setup_services')
64+
65+
assert "Or({Optional('resources'): {Optional('limits'): {Optional('cpus'): Or(<class 'str'>, <class 'float'>, <class 'int'>), Optional('memory'): <class 'str'>}}}, None) did not validate {'resources': {'limits': {'memory': 1}}}" in str(e.value)
66+
assert "1 should be instance of 'str'" in str(e.value)
67+
68+
def test_resource_limits_memory_float():
69+
runner = ScenarioRunner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/resource_limits_memory_float.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
70+
with pytest.raises(SchemaError) as e:
71+
with Tests.RunUntilManager(runner) as context:
72+
context.run_until('setup_services')
73+
74+
assert "1.2 should be instance of 'str'" in str(e.value)
75+
76+
77+
def test_resource_limits_disalign_cpu():
78+
runner = ScenarioRunner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/resource_limits_disalign_cpu.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
79+
with pytest.raises(SchemaError) as e:
80+
with Tests.RunUntilManager(runner) as context:
81+
context.run_until('setup_services')
82+
83+
assert "cpus service top level key and deploy.resources.limits.cpus must be identical" in str(e.value)
84+
85+
def test_resource_limits_disalign_memory():
86+
runner = ScenarioRunner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/resource_limits_disalign_memory.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
87+
with pytest.raises(SchemaError) as e:
88+
with Tests.RunUntilManager(runner) as context:
89+
context.run_until('setup_services')
90+
91+
assert "mem_limit service top level key and deploy.resources.limits.memory must be identical" in str(e.value)

0 commit comments

Comments
 (0)