fd_limit.py 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. """Lift the open-file soft limit to the hard limit at startup (#2883).
  2. Docker, systemd and most native installs start Bambuddy with a soft
  3. ``RLIMIT_NOFILE`` of 1024 and a far higher hard limit. A process may raise its
  4. own soft limit up to the hard one without privileges, so doing it here reaches
  5. every install through the code. A ``ulimits`` block in docker-compose.yml would
  6. not: existing installs keep their own compose file, and on a host whose hard
  7. limit is lower than the block asks for, the container would not start.
  8. At 1024, running out of descriptors is what turned #2883 into a corrupted
  9. database: every new SQLite connection failed with "disk I/O error" for 44
  10. hours. Above 1024 open descriptors the failure is milder. paho-mqtt waits on
  11. its socket with ``select()``, which cannot take a descriptor numbered 1024 or
  12. higher; it reports that as a lost connection and reconnects, so printer
  13. connections drop while the database keeps working.
  14. """
  15. import logging
  16. logger = logging.getLogger(__name__)
  17. # What startup found and did, for the support bundle. None until it has run, or
  18. # on a platform without RLIMIT_NOFILE (Windows).
  19. startup_status: dict | None = None
  20. # Tried in order when the hard limit is unlimited. macOS reports RLIM_INFINITY
  21. # but refuses a soft limit above kern.maxfilesperproc, so a finite value has to
  22. # be picked; 10240 is the usual OPEN_MAX there.
  23. _UNLIMITED_HARD_TARGETS = (65536, 10240)
  24. def _describe(value: int, infinity: int) -> int | str:
  25. return "unlimited" if value == infinity else value
  26. def raise_open_file_limit() -> dict | None:
  27. """Raise the soft open-file limit as far as the hard limit allows.
  28. Never lowers it, and never raises: a limit that cannot be changed is logged
  29. and startup carries on with it. Returns what was found and done, which is
  30. also kept in ``startup_status``.
  31. """
  32. global startup_status
  33. try:
  34. import resource
  35. except ImportError:
  36. return None
  37. infinity = resource.RLIM_INFINITY
  38. try:
  39. soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
  40. except (OSError, ValueError) as e:
  41. logger.warning("Could not read the open-file limit: %s", e)
  42. return None
  43. status: dict = {
  44. "soft_at_start": _describe(soft, infinity),
  45. "hard": _describe(hard, infinity),
  46. "soft": _describe(soft, infinity),
  47. "raised": False,
  48. }
  49. startup_status = status
  50. targets = _UNLIMITED_HARD_TARGETS if hard == infinity else (hard,)
  51. targets = tuple(t for t in targets if soft != infinity and t > soft)
  52. if not targets:
  53. logger.info("Open-file limit: %s (hard limit %s)", status["soft"], status["hard"])
  54. return status
  55. error: Exception | None = None
  56. for target in targets:
  57. try:
  58. resource.setrlimit(resource.RLIMIT_NOFILE, (target, hard))
  59. except (OSError, ValueError) as e:
  60. error = e
  61. continue
  62. status["soft"] = target
  63. status["raised"] = True
  64. logger.info("Raised the open-file limit from %s to %s (hard limit %s)", soft, target, status["hard"])
  65. return status
  66. status["error"] = str(error)
  67. logger.warning(
  68. "Could not raise the open-file limit above %s (hard limit %s): %s",
  69. soft,
  70. status["hard"],
  71. error,
  72. )
  73. return status