mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 09:17:10 +00:00
Increase open file descriptor limit on startup (#148940)
Co-authored-by: Jan Čermák <sairon@sairon.cz> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
ff7c125334
commit
09e7d8d1a5
@ -17,6 +17,7 @@ from . import bootstrap
|
|||||||
from .core import callback
|
from .core import callback
|
||||||
from .helpers.frame import warn_use
|
from .helpers.frame import warn_use
|
||||||
from .util.executor import InterruptibleThreadPoolExecutor
|
from .util.executor import InterruptibleThreadPoolExecutor
|
||||||
|
from .util.resource import set_open_file_descriptor_limit
|
||||||
from .util.thread import deadlock_safe_shutdown
|
from .util.thread import deadlock_safe_shutdown
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -146,6 +147,7 @@ def _enable_posix_spawn() -> None:
|
|||||||
def run(runtime_config: RuntimeConfig) -> int:
|
def run(runtime_config: RuntimeConfig) -> int:
|
||||||
"""Run Home Assistant."""
|
"""Run Home Assistant."""
|
||||||
_enable_posix_spawn()
|
_enable_posix_spawn()
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
|
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
|
||||||
# Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
|
# Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
65
homeassistant/util/resource.py
Normal file
65
homeassistant/util/resource.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""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."""
|
||||||
|
try:
|
||||||
|
# Check environment variable first, then use default
|
||||||
|
soft_limit = int(os.environ.get("SOFT_FILE_LIMIT", DEFAULT_SOFT_FILE_LIMIT))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
except ValueError as err:
|
||||||
|
_LOGGER.error("Invalid file descriptor limit value: %s", err)
|
153
tests/util/test_resource.py
Normal file
153
tests/util/test_resource.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""Test the resource utility module."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.util.resource import (
|
||||||
|
DEFAULT_SOFT_FILE_LIMIT,
|
||||||
|
set_open_file_descriptor_limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("original_soft", "expected_calls", "should_log_already_sufficient"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
1024,
|
||||||
|
[call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))],
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DEFAULT_SOFT_FILE_LIMIT - 1,
|
||||||
|
[call(resource.RLIMIT_NOFILE, (DEFAULT_SOFT_FILE_LIMIT, 524288))],
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(DEFAULT_SOFT_FILE_LIMIT, [], True),
|
||||||
|
(DEFAULT_SOFT_FILE_LIMIT + 1, [], True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_open_file_descriptor_limit_default(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
original_soft: int,
|
||||||
|
expected_calls: list,
|
||||||
|
should_log_already_sufficient: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting file limit with default value."""
|
||||||
|
original_hard = 524288
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.getrlimit",
|
||||||
|
return_value=(original_soft, original_hard),
|
||||||
|
),
|
||||||
|
patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit,
|
||||||
|
):
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
|
|
||||||
|
assert mock_setrlimit.call_args_list == expected_calls
|
||||||
|
assert (
|
||||||
|
f"Current soft limit ({original_soft}) is already" in caplog.text
|
||||||
|
) is should_log_already_sufficient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
(
|
||||||
|
"original_soft",
|
||||||
|
"custom_limit",
|
||||||
|
"expected_calls",
|
||||||
|
"should_log_already_sufficient",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
(1499, 1500, [call(resource.RLIMIT_NOFILE, (1500, 524288))], False),
|
||||||
|
(1500, 1500, [], True),
|
||||||
|
(1501, 1500, [], True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_open_file_descriptor_limit_environment_variable(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
original_soft: int,
|
||||||
|
custom_limit: int,
|
||||||
|
expected_calls: list,
|
||||||
|
should_log_already_sufficient: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting file limit from environment variable."""
|
||||||
|
original_hard = 524288
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(custom_limit)}),
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.getrlimit",
|
||||||
|
return_value=(original_soft, original_hard),
|
||||||
|
),
|
||||||
|
patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit,
|
||||||
|
):
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
|
|
||||||
|
assert mock_setrlimit.call_args_list == expected_calls
|
||||||
|
assert (
|
||||||
|
f"Current soft limit ({original_soft}) is already" in caplog.text
|
||||||
|
) is should_log_already_sufficient
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_file_descriptor_limit_exceeds_hard_limit(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting file limit that exceeds hard limit."""
|
||||||
|
original_soft, original_hard = (1024, 524288)
|
||||||
|
excessive_limit = original_hard + 1
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"SOFT_FILE_LIMIT": str(excessive_limit)}),
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.getrlimit",
|
||||||
|
return_value=(original_soft, original_hard),
|
||||||
|
),
|
||||||
|
patch("homeassistant.util.resource.resource.setrlimit") as mock_setrlimit,
|
||||||
|
):
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
|
|
||||||
|
mock_setrlimit.assert_called_once_with(
|
||||||
|
resource.RLIMIT_NOFILE, (original_hard, original_hard)
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
f"Requested soft limit ({excessive_limit}) exceeds hard limit ({original_hard})"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_file_descriptor_limit_os_error(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling OSError when setting file limit."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.getrlimit",
|
||||||
|
return_value=(1024, 524288),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.setrlimit",
|
||||||
|
side_effect=OSError("Permission denied"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
|
|
||||||
|
assert "Failed to set file descriptor limit" in caplog.text
|
||||||
|
assert "Permission denied" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_open_file_descriptor_limit_value_error(
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling ValueError when setting file limit."""
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"SOFT_FILE_LIMIT": "invalid_value"}),
|
||||||
|
patch(
|
||||||
|
"homeassistant.util.resource.resource.getrlimit",
|
||||||
|
return_value=(1024, 524288),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
set_open_file_descriptor_limit()
|
||||||
|
|
||||||
|
assert "Invalid file descriptor limit value" in caplog.text
|
||||||
|
assert "'invalid_value'" in caplog.text
|
Loading…
x
Reference in New Issue
Block a user