Skip to content

Commit ff64dd8

Browse files
committed
Calculate linter.config.jobs in cgroupsv2 environments
1 parent 68cb5b3 commit ff64dd8

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

pylint/lint/run.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def _query_cpu() -> int | None:
6565
cpu_shares = int(file.read().rstrip())
6666
# For AWS, gives correct value * 1024.
6767
avail_cpu = int(cpu_shares / 1024)
68+
elif Path("/sys/fs/cgroup/cpu.max").is_file():
69+
# Cgroupv2 systems
70+
with open("/sys/fs/cgroup/cpu.max", encoding="utf-8") as file:
71+
line = file.read().rstrip()
72+
fields = line.split()
73+
if len(fields) == 2:
74+
str_cpu_quota = fields[0]
75+
cpu_period = int(fields[1])
76+
# Make sure this is not in an unconstrained cgroup
77+
if str_cpu_quota != "max":
78+
cpu_quota = int(str_cpu_quota)
79+
avail_cpu = int(cpu_quota / cpu_period)
6880

6981
# In K8s Pods also a fraction of a single core could be available
7082
# As multiprocessing is not able to run only a "fraction" of process

tests/lint/test_run.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2+
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
3+
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
4+
5+
from io import BufferedReader
6+
from pathlib import Path
7+
from typing import Any
8+
from unittest.mock import MagicMock, mock_open, patch
9+
10+
import pytest
11+
12+
from pylint import lint
13+
from pylint.testutils.utils import _test_cwd
14+
15+
16+
@pytest.mark.parametrize(
17+
"contents,expected",
18+
[
19+
("50000 100000", 1),
20+
("100000 100000", 1),
21+
("200000 100000", 2),
22+
("299999 100000", 2),
23+
("300000 100000", 3),
24+
# Unconstrained cgroup
25+
("max 100000", None),
26+
],
27+
)
28+
def test_query_cpu_cgroupv2(
29+
tmp_path: Path,
30+
contents: str,
31+
expected: int,
32+
) -> None:
33+
"""Check that `pylint.lint.run._query_cpu` generates realistic values in cgroupsv2 systems."""
34+
builtin_open = open
35+
36+
def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
37+
if args[0] == "/sys/fs/cgroup/cpu.max":
38+
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
39+
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]
40+
41+
pathlib_path = Path
42+
43+
def _mock_path(*args: str, **kwargs: Any) -> Path:
44+
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
45+
return MagicMock(is_file=lambda: False)
46+
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
47+
return MagicMock(is_file=lambda: False)
48+
if args[0] == "/sys/fs/cgroup/cpu.max":
49+
return MagicMock(is_file=lambda: True)
50+
return pathlib_path(*args, **kwargs)
51+
52+
with _test_cwd(tmp_path):
53+
with patch("builtins.open", _mock_open):
54+
with patch("pylint.lint.run.Path", _mock_path):
55+
cpus = lint.run._query_cpu()
56+
assert cpus == expected

tests/test_pylint_runners.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,45 @@ def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
100100
with patch("pylint.lint.run.Path", _mock_path):
101101
Run(testargs, reporter=Reporter())
102102
assert err.value.code == 0
103+
104+
105+
@pytest.mark.parametrize(
106+
"contents",
107+
[
108+
"1 2",
109+
"max 100000",
110+
],
111+
)
112+
def test_pylint_run_jobs_equal_zero_dont_crash_with_cgroupv2(
113+
tmp_path: pathlib.Path,
114+
contents: str,
115+
) -> None:
116+
"""Check that the pylint runner does not crash if `pylint.lint.run._query_cpu`
117+
determines only a fraction of a CPU core to be available.
118+
"""
119+
builtin_open = open
120+
121+
def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
122+
if args[0] == "/sys/fs/cgroup/cpu.max":
123+
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
124+
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]
125+
126+
pathlib_path = pathlib.Path
127+
128+
def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
129+
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
130+
return MagicMock(is_file=lambda: False)
131+
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
132+
return MagicMock(is_file=lambda: False)
133+
if args[0] == "/sys/fs/cgroup/cpu.max":
134+
return MagicMock(is_file=lambda: True)
135+
return pathlib_path(*args, **kwargs)
136+
137+
filepath = os.path.abspath(__file__)
138+
testargs = [filepath, "--jobs=0"]
139+
with _test_cwd(tmp_path):
140+
with pytest.raises(SystemExit) as err:
141+
with patch("builtins.open", _mock_open):
142+
with patch("pylint.lint.run.Path", _mock_path):
143+
Run(testargs, reporter=Reporter())
144+
assert err.value.code == 0

0 commit comments

Comments
 (0)