Increase open file descriptor limit on startup

Increase the open file descriptor soft limit to 2048 on startup. This
is necessary to prevent issues with file descriptor exhaustion in
environments where the soft limit is low (e.g. the defaul of 1024 on
Linux and since Home Assistant OS 16.0 in its container environment).
This commit is contained in:
Stefan Agner 2025-07-17 11:28:21 +02:00
parent 72d1c3cfc8
commit 8a0ee762a6
No known key found for this signature in database
GPG Key ID: AE01353D1E44747D
3 changed files with 158 additions and 0 deletions

View File

@ -111,6 +111,7 @@ from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
from .util.resource import set_open_file_descriptor_limit
from .util.system_info import is_official_image
with contextlib.suppress(ImportError):
@ -302,6 +303,8 @@ async def async_setup_hass(
hass = await create_hass()
set_open_file_descriptor_limit()
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
_LOGGER.warning(
"Skipping pip installation of required modules. This may cause issues"

View File

@ -0,0 +1,63 @@
"""Resource management utilities for Home Assistant."""
from __future__ import annotations
import logging
import os
import resource
from typing import Final
_LOGGER = logging.getLogger(__name__)
# Default soft file descriptor limit to set
DEFAULT_SOFT_FILE_LIMIT: Final = 2048
def set_open_file_descriptor_limit() -> None:
"""Set the maximum open file descriptor soft limit."""
# Check environment variable first, then use default
soft_limit = int(os.environ.get("SOFT_FILE_LIMIT", DEFAULT_SOFT_FILE_LIMIT))
try:
# Get current limits
current_soft, current_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
_LOGGER.debug(
"Current file descriptor limits: soft=%d, hard=%d",
current_soft,
current_hard,
)
# Don't increase if already at or above the desired limit
if current_soft >= soft_limit:
_LOGGER.debug(
"Current soft limit (%d) is already >= desired limit (%d), skipping",
current_soft,
soft_limit,
)
return
# Don't set soft limit higher than hard limit
if soft_limit > current_hard:
_LOGGER.warning(
"Requested soft limit (%d) exceeds hard limit (%d), "
"setting to hard limit",
soft_limit,
current_hard,
)
soft_limit = current_hard
# Set the new soft limit
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, current_hard))
# Verify the change
new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
_LOGGER.info(
"File descriptor limits updated: soft=%d->%d, hard=%d",
current_soft,
new_soft,
new_hard,
)
except OSError as err:
_LOGGER.error("Failed to set file descriptor limit: %s", err)

View File

@ -0,0 +1,92 @@
"""Test the resource utility module."""
import os
import resource
from unittest.mock import patch
from homeassistant.util.resource import (
DEFAULT_SOFT_FILE_LIMIT,
set_open_file_descriptor_limit,
)
def test_set_open_file_descriptor_limit_default() -> None:
"""Test setting file limit with default value."""
original_soft, original_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
with patch("homeassistant.util.resource._LOGGER") as mock_logger:
set_open_file_descriptor_limit()
# Check that we attempted to set the limit
new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
# If the original soft limit was already >= DEFAULT_SOFT_FILE_LIMIT,
# it should remain unchanged
if original_soft >= DEFAULT_SOFT_FILE_LIMIT:
assert new_soft == original_soft
mock_logger.debug.assert_called()
else:
# Should have been increased to DEFAULT_SOFT_FILE_LIMIT or hard limit
expected_soft = min(DEFAULT_SOFT_FILE_LIMIT, original_hard)
assert new_soft == expected_soft
mock_logger.info.assert_called()
def test_set_open_file_descriptor_limit_environment_variable() -> None:
"""Test setting file limit from environment variable."""
custom_limit = 1500
with (
patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(custom_limit)}),
patch("homeassistant.util.resource._LOGGER") as mock_logger,
):
original_soft, original_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
set_open_file_descriptor_limit()
new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if original_soft >= custom_limit:
assert new_soft == original_soft
mock_logger.debug.assert_called()
else:
expected_soft = min(custom_limit, original_hard)
assert new_soft == expected_soft
def test_set_open_file_descriptor_limit_exceeds_hard_limit() -> None:
"""Test setting file limit that exceeds hard limit."""
original_soft, original_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
excessive_limit = original_hard + 1000
with (
patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(excessive_limit)}),
patch("homeassistant.util.resource._LOGGER") as mock_logger,
):
set_open_file_descriptor_limit()
new_soft, new_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
# Should be capped at hard limit
assert new_soft == original_hard
mock_logger.warning.assert_called_once()
def test_set_open_file_descriptor_limit_os_error() -> None:
"""Test handling OSError when setting file limit."""
with (
patch(
"homeassistant.util.resource.resource.getrlimit", return_value=(1000, 4096)
),
patch(
"homeassistant.util.resource.resource.setrlimit",
side_effect=OSError("Permission denied"),
),
patch("homeassistant.util.resource._LOGGER") as mock_logger,
):
set_open_file_descriptor_limit()
mock_logger.error.assert_called_once()
assert (
"Failed to set file descriptor limit" in mock_logger.error.call_args[0][0]
)