Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate linter.config.jobs in cgroupsv2 environments #10089

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions custom_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ contextlib
contextmanager
contravariance
contravariant
cgroup
CPython
cpython
csv
Expand Down
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/10103.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixes a crash that occurred when pylint was run in a container on a host with cgroupsv2 and restrictions on CPU usage.

Closes #10103
27 changes: 26 additions & 1 deletion pylint/lint/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,30 @@ def _query_cpu() -> int | None:
This is based on discussion and copied from suggestions in
https://bugs.python.org/issue36054.
"""
cpu_quota, avail_cpu = None, None
if Path("/sys/fs/cgroup/cpu.max").is_file():
avail_cpu = _query_cpu_cgroupv2()
else:
avail_cpu = _query_cpu_cgroupsv1()
return _query_cpu_handle_k8s_pods(avail_cpu)


def _query_cpu_cgroupv2() -> int | None:
avail_cpu = None
with open("/sys/fs/cgroup/cpu.max", encoding="utf-8") as file:
line = file.read().rstrip()
fields = line.split()
if len(fields) == 2:
str_cpu_quota = fields[0]
cpu_period = int(fields[1])
# Make sure this is not in an unconstrained cgroup
if str_cpu_quota != "max":
cpu_quota = int(str_cpu_quota)
avail_cpu = int(cpu_quota / cpu_period)
return avail_cpu


def _query_cpu_cgroupsv1() -> int | None:
cpu_quota, avail_cpu = None, None
if Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").is_file():
with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8") as file:
# Not useful for AWS Batch based jobs as result is -1, but works on local linux systems
Expand All @@ -65,7 +87,10 @@ def _query_cpu() -> int | None:
cpu_shares = int(file.read().rstrip())
# For AWS, gives correct value * 1024.
avail_cpu = int(cpu_shares / 1024)
return avail_cpu


def _query_cpu_handle_k8s_pods(avail_cpu: int | None) -> int | None:
# In K8s Pods also a fraction of a single core could be available
# As multiprocessing is not able to run only a "fraction" of process
# assume we have 1 CPU available
Expand Down
90 changes: 90 additions & 0 deletions tests/test_pylint_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import pytest

from pylint import run_pylint, run_pyreverse, run_symilar
from pylint.lint.run import _query_cpu
from pylint.testutils import GenericTestReporter as Reporter
from pylint.testutils._run import _Run as Run
from pylint.testutils.utils import _test_cwd
Expand Down Expand Up @@ -90,6 +91,8 @@ def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
return MagicMock(is_file=lambda: True)
if args[0] == "/sys/fs/cgroup/cpu.max":
return MagicMock(is_file=lambda: False)
return pathlib_path(*args, **kwargs)

filepath = os.path.abspath(__file__)
Expand All @@ -100,3 +103,90 @@ def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
with patch("pylint.lint.run.Path", _mock_path):
Run(testargs, reporter=Reporter())
assert err.value.code == 0


@pytest.mark.parametrize(
"contents",
[
"1 2",
"max 100000",
],
)
def test_pylint_run_jobs_equal_zero_dont_crash_with_cgroupv2(
tmp_path: pathlib.Path,
contents: str,
) -> None:
"""Check that the pylint runner does not crash if `pylint.lint.run._query_cpu`
determines only a fraction of a CPU core to be available.
"""
builtin_open = open

def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
if args[0] == "/sys/fs/cgroup/cpu.max":
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]

pathlib_path = pathlib.Path

def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
return MagicMock(is_file=lambda: False)
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
return MagicMock(is_file=lambda: False)
if args[0] == "/sys/fs/cgroup/cpu.max":
return MagicMock(is_file=lambda: True)
return pathlib_path(*args, **kwargs)

filepath = os.path.abspath(__file__)
testargs = [filepath, "--jobs=0"]
with _test_cwd(tmp_path):
with pytest.raises(SystemExit) as err:
with patch("builtins.open", _mock_open):
with patch("pylint.lint.run.Path", _mock_path):
Run(testargs, reporter=Reporter())
assert err.value.code == 0


@pytest.mark.parametrize(
"contents,expected",
[
("50000 100000", 1),
("100000 100000", 1),
("200000 100000", 2),
("299999 100000", 2),
("300000 100000", 3),
# Unconstrained cgroup
("max 100000", None),
],
)
def test_query_cpu_cgroupv2(
tmp_path: pathlib.Path,
contents: str,
expected: int,
) -> None:
"""Check that `pylint.lint.run._query_cpu` generates realistic values in cgroupsv2
systems.
"""
builtin_open = open

def _mock_open(*args: Any, **kwargs: Any) -> BufferedReader:
if args[0] == "/sys/fs/cgroup/cpu.max":
return mock_open(read_data=contents)(*args, **kwargs) # type: ignore[no-any-return]
return builtin_open(*args, **kwargs) # type: ignore[no-any-return]

pathlib_path = pathlib.Path

def _mock_path(*args: str, **kwargs: Any) -> pathlib.Path:
if args[0] == "/sys/fs/cgroup/cpu/cpu.shares":
return MagicMock(is_file=lambda: False)
if args[0] == "/sys/fs/cgroup/cpu/cfs_quota_us":
return MagicMock(is_file=lambda: False)
if args[0] == "/sys/fs/cgroup/cpu.max":
return MagicMock(is_file=lambda: True)
return pathlib_path(*args, **kwargs)

with _test_cwd(tmp_path):
with patch("builtins.open", _mock_open):
with patch("pylint.lint.run.Path", _mock_path):
cpus = _query_cpu()
assert cpus == expected
Loading