Files
bambuddy/backend/tests/unit/test_fd_limit_2883.py
T

166 lines
5.5 KiB
Python

"""The open-file limit is raised at startup, and bundles say what holds it (#2883).
#2883 ran out of descriptors at Docker's default soft limit of 1024. From then
on every new SQLite connection failed with "disk I/O error", and after 44 hours
the database was corrupt. The soft limit is the process's own to raise up to the
hard limit, so startup does that on every install. The support bundle now counts
descriptors by kind, so the next report shows what held them.
"""
import resource
import sqlite3
from unittest.mock import MagicMock, patch
import pytest
from backend.app.core import fd_limit
INF = resource.RLIM_INFINITY
@pytest.fixture(autouse=True)
def _reset_status():
fd_limit.startup_status = None
yield
fd_limit.startup_status = None
def _run(soft, hard, setrlimit=None):
calls = []
def _set(which, limits):
calls.append(limits)
if setrlimit is not None:
setrlimit(limits)
with (
patch.object(resource, "getrlimit", return_value=(soft, hard)),
patch.object(resource, "setrlimit", side_effect=_set),
):
status = fd_limit.raise_open_file_limit()
return status, calls
class TestRaiseOpenFileLimit:
def test_the_soft_limit_is_raised_to_the_hard_one(self):
status, calls = _run(1024, 524288)
assert calls == [(524288, 524288)]
assert status == {"soft_at_start": 1024, "hard": 524288, "soft": 524288, "raised": True}
assert fd_limit.startup_status == status
def test_a_limit_already_at_the_hard_one_is_left_alone(self):
status, calls = _run(524288, 524288)
assert calls == []
assert status["raised"] is False
def test_an_unlimited_soft_limit_is_left_alone(self):
status, calls = _run(INF, INF)
assert calls == []
assert status["soft"] == "unlimited"
def test_an_unlimited_hard_limit_gets_a_finite_soft_one(self):
"""macOS reports RLIM_INFINITY but refuses a soft limit above
kern.maxfilesperproc, so finite values are tried in turn."""
def refuse_the_first(limits):
if limits[0] == 65536:
raise ValueError("not allowed")
status, calls = _run(256, INF, setrlimit=refuse_the_first)
assert calls == [(65536, INF), (10240, INF)]
assert status["soft"] == 10240
assert status["hard"] == "unlimited"
assert status["raised"] is True
def test_a_refused_raise_is_logged_and_startup_carries_on(self, caplog):
def refuse(limits):
raise OSError("operation not permitted")
status, _ = _run(1024, 4096, setrlimit=refuse)
assert status["raised"] is False
assert status["soft"] == 1024
assert "operation not permitted" in status["error"]
assert any("Could not raise the open-file limit" in r.getMessage() for r in caplog.records)
def test_an_unreadable_limit_is_not_fatal(self):
with patch.object(resource, "getrlimit", side_effect=OSError("nope")):
assert fd_limit.raise_open_file_limit() is None
def test_no_rlimit_module_is_not_fatal(self):
"""Windows has no ``resource`` module."""
import builtins
real_import = builtins.__import__
def no_resource(name, *args, **kwargs):
if name == "resource":
raise ImportError(name)
return real_import(name, *args, **kwargs)
with patch("builtins.__import__", side_effect=no_resource):
assert fd_limit.raise_open_file_limit() is None
class TestDescriptorsInTheSupportBundle:
@pytest.mark.parametrize(
("target", "kind"),
[
("socket:[12345]", "socket"),
("pipe:[678]", "pipe"),
("anon_inode:[eventpoll]", "anon_inode"),
("/app/data/bambuddy.db", "database"),
("/app/data/bambuddy.db-wal", "database_wal"),
("/app/data/bambuddy.db-shm", "database_shm"),
("/dev/null", "device"),
("/app/logs/bambuddy.log", "file"),
],
)
def test_kinds(self, target, kind):
from backend.app.api.routes.support import _fd_kind
assert _fd_kind(target) == kind
def test_counts_by_kind_against_the_limit_without_paths(self, tmp_path):
import psutil
from backend.app.api.routes.support import _collect_fd_info
db = sqlite3.connect(tmp_path / "secret-name.db")
db.execute("PRAGMA journal_mode = WAL")
db.execute("CREATE TABLE t (x)")
try:
info = _collect_fd_info(psutil.Process())
finally:
db.close()
assert info["num_fds"] > 0
assert info["fds_by_type"]["database"] >= 1
assert info["fds_by_type"]["database_wal"] >= 1
assert set(info["fd_limit"]) == {"soft", "hard"}
assert "secret-name" not in repr(info)
def test_the_startup_result_is_carried(self):
from backend.app.api.routes.support import _collect_fd_info
fd_limit.startup_status = {"soft_at_start": 1024, "hard": 524288, "soft": 524288, "raised": True}
info = _collect_fd_info(MagicMock())
assert info["fd_limit_at_startup"]["soft_at_start"] == 1024
def test_a_failing_probe_still_returns(self):
from backend.app.api.routes.support import _collect_fd_info
proc = MagicMock()
proc.num_fds.side_effect = RuntimeError("restricted")
with patch("os.listdir", side_effect=PermissionError("no /proc")):
info = _collect_fd_info(proc)
assert "num_fds" not in info
assert "fds_by_type" not in info