Add blockbuster option to API (#5746)

* Add blockbuster option to API

* cache not lru_cache
This commit is contained in:
Mike Degatano 2025-03-25 04:40:43 -04:00 committed by GitHub
parent ec721c41c1
commit 80f7f07341
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 135 additions and 6 deletions

View File

@ -11,10 +11,12 @@ import zlib_fast
# Enable fast zlib before importing supervisor
zlib_fast.enable()
from supervisor import bootstrap # pylint: disable=wrong-import-position # noqa: E402
from supervisor.utils.logging import ( # pylint: disable=wrong-import-position # noqa: E402
activate_log_queue_handler,
)
# pylint: disable=wrong-import-position
from supervisor import bootstrap # noqa: E402
from supervisor.utils.blockbuster import activate_blockbuster # noqa: E402
from supervisor.utils.logging import activate_log_queue_handler # noqa: E402
# pylint: enable=wrong-import-position
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -52,6 +54,8 @@ if __name__ == "__main__":
_LOGGER.info("Initializing Supervisor setup")
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
loop.set_debug(coresys.config.debug)
if coresys.config.detect_blocking_io:
activate_blockbuster()
loop.run_until_complete(coresys.core.connect())
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))

View File

@ -80,3 +80,11 @@ class BootSlot(StrEnum):
A = "A"
B = "B"
class DetectBlockingIO(StrEnum):
"""Enable/Disable detection for blocking I/O in event loop."""
OFF = "off"
ON = "on"
ON_AT_STARTUP = "on_at_startup"

View File

@ -20,6 +20,7 @@ from ..const import (
ATTR_CPU_PERCENT,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_FORCE_SECURITY,
ATTR_HEALTHY,
@ -47,10 +48,15 @@ from ..const import (
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..store.validate import repositories
from ..utils.blockbuster import (
activate_blockbuster,
blockbuster_enabled,
deactivate_blockbuster,
)
from ..utils.sentry import close_sentry, init_sentry
from ..utils.validate import validate_timezone
from ..validate import version_tag, wait_boot
from .const import CONTENT_TYPE_TEXT
from .const import CONTENT_TYPE_TEXT, DetectBlockingIO
from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -69,6 +75,7 @@ SCHEMA_OPTIONS = vol.Schema(
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
}
)
@ -101,6 +108,7 @@ class APISupervisor(CoreSysAttributes):
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
ATTR_DETECT_BLOCKING_IO: blockbuster_enabled(),
# Depricated
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
ATTR_ADDONS: [
@ -160,6 +168,17 @@ class APISupervisor(CoreSysAttributes):
if ATTR_AUTO_UPDATE in body:
self.sys_updater.auto_update = body[ATTR_AUTO_UPDATE]
if detect_blocking_io := body.get(ATTR_DETECT_BLOCKING_IO):
if detect_blocking_io == DetectBlockingIO.ON_AT_STARTUP:
self.sys_config.detect_blocking_io = True
detect_blocking_io = DetectBlockingIO.ON
if detect_blocking_io == DetectBlockingIO.ON:
activate_blockbuster()
elif detect_blocking_io == DetectBlockingIO.OFF:
self.sys_config.detect_blocking_io = False
deactivate_blockbuster()
# Deprecated
if ATTR_WAIT_BOOT in body:
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]

View File

@ -12,6 +12,7 @@ from .const import (
ATTR_ADDONS_CUSTOM_LIST,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_IMAGE,
ATTR_LAST_BOOT,
@ -142,6 +143,16 @@ class CoreConfig(FileConfiguration):
"""Set debug wait mode."""
self._data[ATTR_DEBUG_BLOCK] = value
@property
def detect_blocking_io(self) -> bool:
"""Return True if blocking I/O in event loop detection enabled at startup."""
return self._data[ATTR_DETECT_BLOCKING_IO]
@detect_blocking_io.setter
def detect_blocking_io(self, value: bool) -> None:
"""Enable/Disable blocking I/O in event loop detection at startup."""
self._data[ATTR_DETECT_BLOCKING_IO] = value
@property
def diagnostics(self) -> bool | None:
"""Return bool if diagnostics is set otherwise None."""

View File

@ -152,6 +152,7 @@ ATTR_DEFAULT = "default"
ATTR_DEPLOYMENT = "deployment"
ATTR_DESCRIPTON = "description"
ATTR_DETACHED = "detached"
ATTR_DETECT_BLOCKING_IO = "detect_blocking_io"
ATTR_DEVICES = "devices"
ATTR_DEVICETREE = "devicetree"
ATTR_DIAGNOSTICS = "diagnostics"

View File

@ -0,0 +1,35 @@
"""Activate and deactivate blockbuster for finding blocking I/O."""
from functools import cache
import logging
from blockbuster import BlockBuster
_LOGGER: logging.Logger = logging.getLogger(__name__)
@cache
def _get_blockbuster() -> BlockBuster:
"""Get blockbuster instance."""
return BlockBuster()
def blockbuster_enabled() -> bool:
"""Return true if blockbuster detection is enabled."""
blockbuster = _get_blockbuster()
# We activate all or none so just check the first one
for _, fn in blockbuster.functions.items():
return fn.activated
return False
def activate_blockbuster() -> None:
"""Activate blockbuster detection."""
_LOGGER.info("Activating BlockBuster blocking I/O detection")
_get_blockbuster().activate()
def deactivate_blockbuster() -> None:
"""Deactivate blockbuster detection."""
_LOGGER.info("Deactivating BlockBuster blocking I/O detection")
_get_blockbuster().deactivate()

View File

@ -15,6 +15,7 @@ from .const import (
ATTR_CONTENT_TRUST,
ATTR_DEBUG,
ATTR_DEBUG_BLOCK,
ATTR_DETECT_BLOCKING_IO,
ATTR_DIAGNOSTICS,
ATTR_DISPLAYNAME,
ATTR_DNS,
@ -162,6 +163,7 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema(
vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(),
vol.Optional(ATTR_DEBUG_BLOCK, default=False): vol.Boolean(),
vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()),
vol.Optional(ATTR_DETECT_BLOCKING_IO, default=False): vol.Boolean(),
},
extra=vol.REMOVE_EXTRA,
)

View File

@ -1,9 +1,11 @@
"""Test Supervisor API."""
# pylint: disable=protected-access
import time
from unittest.mock import MagicMock, patch
from aiohttp.test_utils import TestClient
from blockbuster import BlockingError
import pytest
from supervisor.coresys import CoreSys
@ -247,3 +249,46 @@ async def test_api_supervisor_options_timezone(
assert resp.status == 200
assert coresys.timezone == "Europe/Zurich"
@pytest.mark.parametrize(
("blockbuster", "option_value", "config_value"),
[("no_blockbuster", "on", False), ("no_blockbuster", "on_at_startup", True)],
indirect=["blockbuster"],
)
async def test_api_supervisor_options_blocking_io(
api_client: TestClient, coresys: CoreSys, option_value: str, config_value: bool
):
"""Test setting supervisor detect blocking io option."""
# This should not fail with a blocking error yet
time.sleep(0)
resp = await api_client.post(
"/supervisor/options", json={"detect_blocking_io": option_value}
)
assert resp.status == 200
resp = await api_client.get("/supervisor/info")
assert resp.status == 200
body = await resp.json()
assert body["data"]["detect_blocking_io"] is True
# This remains false because we only turned it on for current run of supervisor, not permanently
assert coresys.config.detect_blocking_io is config_value
with pytest.raises(BlockingError):
time.sleep(0)
resp = await api_client.post(
"/supervisor/options", json={"detect_blocking_io": "off"}
)
assert resp.status == 200
resp = await api_client.get("/supervisor/info")
assert resp.status == 200
body = await resp.json()
assert body["data"]["detect_blocking_io"] is False
assert coresys.config.detect_blocking_io is False
# This should not raise blocking error anymore
time.sleep(0)

View File

@ -65,8 +65,12 @@ from .dbus_service_mocks.network_manager import NetworkManager as NetworkManager
@pytest.fixture(autouse=True)
def blockbuster() -> BlockBuster:
def blockbuster(request: pytest.FixtureRequest) -> BlockBuster | None:
"""Raise for blocking I/O in event loop."""
if getattr(request, "param", "") == "no_blockbuster":
yield None
return
# Only scanning supervisor code for now as that's our primary interest
# This will still raise for tests that call utilities in supervisor code that block
# But it will ignore calls to libraries and such that do blocking I/O directly from tests