Skip to content

Commit ad9f26c

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

File tree

3 files changed

+101
-0
lines changed

3 files changed

+101
-0
lines changed

custom_dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ contextlib
6868
contextmanager
6969
contravariance
7070
contravariant
71+
cgroup
7172
CPython
7273
cpython
7374
csv

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/test_pylint_runners.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import pytest
1919

2020
from pylint import run_pylint, run_pyreverse, run_symilar
21+
from pylint.lint.run import _query_cpu
2122
from pylint.testutils import GenericTestReporter as Reporter
2223
from pylint.testutils._run import _Run as Run
2324
from pylint.testutils.utils import _test_cwd
@@ -100,3 +101,90 @@ def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
100101
with patch("pylint.lint.run.Path", _mock_path):
101102
Run(testargs, reporter=Reporter())
102103
assert err.value.code == 0
104+
105+
106+
@pytest.mark.parametrize(
107+
"contents",
108+
[
109+
"1 2",
110+
"max 100000",
111+
],
112+
)
113+
def test_pylint_run_jobs_equal_zero_dont_crash_with_cgroupv2(
114+
tmp_path: pathlib.Path,
115+
contents: str,
116+
) -> None:
117+
"""Check that the pylint runner does not crash if `pylint.lint.run._query_cpu`
118+
determines only a fraction of a CPU core to be available.
119+
"""
120+
builtin_open = open
121+
122+
def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
123+
if args[0] == "/sys/fs/cgroup/cpu.max":
124+
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
125+
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]
126+
127+
pathlib_path = pathlib.Path
128+
129+
def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
130+
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
131+
return MagicMock(is_file=lambda: False)
132+
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
133+
return MagicMock(is_file=lambda: False)
134+
if args[0] == "/sys/fs/cgroup/cpu.max":
135+
return MagicMock(is_file=lambda: True)
136+
return pathlib_path(*args, **kwargs)
137+
138+
filepath = os.path.abspath(__file__)
139+
testargs = [filepath, "--jobs=0"]
140+
with _test_cwd(tmp_path):
141+
with pytest.raises(SystemExit) as err:
142+
with patch("builtins.open", _mock_open):
143+
with patch("pylint.lint.run.Path", _mock_path):
144+
Run(testargs, reporter=Reporter())
145+
assert err.value.code == 0
146+
147+
148+
@pytest.mark.parametrize(
149+
"contents,expected",
150+
[
151+
("50000 100000", 1),
152+
("100000 100000", 1),
153+
("200000 100000", 2),
154+
("299999 100000", 2),
155+
("300000 100000", 3),
156+
# Unconstrained cgroup
157+
("max 100000", None),
158+
],
159+
)
160+
def test_query_cpu_cgroupv2(
161+
tmp_path: pathlib.Path,
162+
contents: str,
163+
expected: int,
164+
) -> None:
165+
"""Check that `pylint.lint.run._query_cpu` generates realistic values in cgroupsv2
166+
systems.
167+
"""
168+
builtin_open = open
169+
170+
def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
171+
if args[0] == "/sys/fs/cgroup/cpu.max":
172+
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
173+
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]
174+
175+
pathlib_path = pathlib.Path
176+
177+
def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
178+
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
179+
return MagicMock(is_file=lambda: False)
180+
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
181+
return MagicMock(is_file=lambda: False)
182+
if args[0] == "/sys/fs/cgroup/cpu.max":
183+
return MagicMock(is_file=lambda: True)
184+
return pathlib_path(*args, **kwargs)
185+
186+
with _test_cwd(tmp_path):
187+
with patch("builtins.open", _mock_open):
188+
with patch("pylint.lint.run.Path", _mock_path):
189+
cpus = _query_cpu()
190+
assert cpus == expected

0 commit comments

Comments
 (0)