mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
166 lines
5.5 KiB
Python
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
|