| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- """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
|