Skip to content
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
15 changes: 10 additions & 5 deletions src/crawlee/_utils/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,19 @@ def get_memory_info() -> MemoryInfo:
logger.debug('Calling get_memory_info()...')
current_process = psutil.Process(os.getpid())

# Retrieve estimated memory usage of the current process.
current_size_bytes = _get_used_memory(current_process)
# Retrieve estimated memory usage of the current process. On Linux `_get_used_memory` reads PSS via
# `memory_full_info`, which can raise `AccessDenied` in restricted environments (e.g. hardened containers);
# fall back to RSS, which a process can always read for itself.
try:
current_size_bytes = _get_used_memory(current_process)
except psutil.AccessDenied:
current_size_bytes = int(current_process.memory_info().rss)

# Sum memory usage by all children processes, try to exclude shared memory from the sum if allowed by OS.
for child in current_process.children(recursive=True):
# Ignore any NoSuchProcess exception that might occur if a child process ends before we retrieve
# its memory usage.
with suppress(psutil.NoSuchProcess):
# Ignore a child that ends before we retrieve its memory usage (`NoSuchProcess`) or that we are not
# allowed to inspect (`AccessDenied`, e.g. an unreadable subprocess in a restricted environment).
with suppress(psutil.NoSuchProcess, psutil.AccessDenied):
Comment on lines 133 to +137
current_size_bytes += _get_used_memory(child)

vm = psutil.virtual_memory()
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/_utils/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from multiprocessing.shared_memory import SharedMemory
from typing import TYPE_CHECKING

import psutil
import pytest

from crawlee._utils import system
from crawlee._utils.byte_size import ByteSize
from crawlee._utils.system import get_cpu_info, get_memory_info

Expand All @@ -21,6 +23,49 @@ def test_get_memory_info_returns_valid_values() -> None:
assert memory_info.current_size < memory_info.total_size


def test_get_memory_info_skips_children_with_access_denied(monkeypatch: pytest.MonkeyPatch) -> None:
"""A child process we are not allowed to inspect must be skipped, not abort the whole snapshot.

In restricted environments (e.g. hardened containers) reading a child's memory can raise
`psutil.AccessDenied`, which is not a subclass of `psutil.NoSuchProcess` and so was not suppressed.
"""
child = psutil.Process() # any process object works; only `_get_used_memory` behavior matters here

monkeypatch.setattr(psutil.Process, 'children', lambda *_args, **_kwargs: [child])

def fake_get_used_memory(process: psutil.Process) -> int:
if process is child:
raise psutil.AccessDenied(pid=child.pid)
return 100

monkeypatch.setattr(system, '_get_used_memory', fake_get_used_memory)

memory_info = get_memory_info()

# The unreadable child is skipped, so only the current process (100) is counted.
assert memory_info.current_size == ByteSize(100)


def test_get_memory_info_falls_back_to_rss_when_current_process_access_denied(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""If PSS for the current process is denied, fall back to RSS instead of crashing.

On Linux `_get_used_memory` reads PSS via `memory_full_info`, which may require elevated privileges.
"""
monkeypatch.setattr(psutil.Process, 'children', lambda *_args, **_kwargs: [])

def fake_get_used_memory(process: psutil.Process) -> int:
raise psutil.AccessDenied(pid=process.pid)

monkeypatch.setattr(system, '_get_used_memory', fake_get_used_memory)

memory_info = get_memory_info()

# RSS of the current process is a positive value below total system memory.
assert ByteSize(0) < memory_info.current_size < memory_info.total_size


def test_get_cpu_info_returns_valid_values() -> None:
cpu_info = get_cpu_info()
assert 0 <= cpu_info.used_ratio <= 1
Expand Down
Loading