Skip to content

Commit 733ed3e

Browse files
committed
Add blockbuster option to API
1 parent 3475246 commit 733ed3e

File tree

9 files changed

+135
-6
lines changed

9 files changed

+135
-6
lines changed

supervisor/__main__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
# Enable fast zlib before importing supervisor
1212
zlib_fast.enable()
1313

14-
from supervisor import bootstrap # pylint: disable=wrong-import-position # noqa: E402
15-
from supervisor.utils.logging import ( # pylint: disable=wrong-import-position # noqa: E402
16-
activate_log_queue_handler,
17-
)
14+
# pylint: disable=wrong-import-position
15+
from supervisor import bootstrap # noqa: E402
16+
from supervisor.utils.blockbuster import activate_blockbuster # noqa: E402
17+
from supervisor.utils.logging import activate_log_queue_handler # noqa: E402
18+
19+
# pylint: enable=wrong-import-position
1820

1921
_LOGGER: logging.Logger = logging.getLogger(__name__)
2022

@@ -52,6 +54,8 @@ def run_os_startup_check_cleanup() -> None:
5254
_LOGGER.info("Initializing Supervisor setup")
5355
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
5456
loop.set_debug(coresys.config.debug)
57+
if coresys.config.detect_blocking_io:
58+
activate_blockbuster()
5559
loop.run_until_complete(coresys.core.connect())
5660

5761
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))

supervisor/api/const.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,11 @@ class BootSlot(StrEnum):
8080

8181
A = "A"
8282
B = "B"
83+
84+
85+
class DetectBlockingIO(StrEnum):
86+
"""Enable/Disable detection for blocking I/O in event loop."""
87+
88+
OFF = "off"
89+
ON = "on"
90+
ON_AT_STARTUP = "on_at_startup"

supervisor/api/supervisor.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ATTR_CPU_PERCENT,
2121
ATTR_DEBUG,
2222
ATTR_DEBUG_BLOCK,
23+
ATTR_DETECT_BLOCKING_IO,
2324
ATTR_DIAGNOSTICS,
2425
ATTR_FORCE_SECURITY,
2526
ATTR_HEALTHY,
@@ -47,10 +48,15 @@
4748
from ..coresys import CoreSysAttributes
4849
from ..exceptions import APIError
4950
from ..store.validate import repositories
51+
from ..utils.blockbuster import (
52+
activate_blockbuster,
53+
blockbuster_enabled,
54+
deactivate_blockbuster,
55+
)
5056
from ..utils.sentry import close_sentry, init_sentry
5157
from ..utils.validate import validate_timezone
5258
from ..validate import version_tag, wait_boot
53-
from .const import CONTENT_TYPE_TEXT
59+
from .const import CONTENT_TYPE_TEXT, DetectBlockingIO
5460
from .utils import api_process, api_process_raw, api_validate
5561

5662
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -69,6 +75,7 @@
6975
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
7076
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
7177
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
78+
vol.Optional(ATTR_DETECT_BLOCKING_IO): vol.Coerce(DetectBlockingIO),
7279
}
7380
)
7481

@@ -101,6 +108,7 @@ async def info(self, request: web.Request) -> dict[str, Any]:
101108
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
102109
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
103110
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
111+
ATTR_DETECT_BLOCKING_IO: blockbuster_enabled(),
104112
# Depricated
105113
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
106114
ATTR_ADDONS: [
@@ -160,6 +168,17 @@ async def options(self, request: web.Request) -> None:
160168
if ATTR_AUTO_UPDATE in body:
161169
self.sys_updater.auto_update = body[ATTR_AUTO_UPDATE]
162170

171+
if detect_blocking_io := body.get(ATTR_DETECT_BLOCKING_IO):
172+
if detect_blocking_io == DetectBlockingIO.ON_AT_STARTUP:
173+
self.sys_config.detect_blocking_io = True
174+
detect_blocking_io = DetectBlockingIO.ON
175+
176+
if detect_blocking_io == DetectBlockingIO.ON:
177+
activate_blockbuster()
178+
elif detect_blocking_io == DetectBlockingIO.OFF:
179+
self.sys_config.detect_blocking_io = False
180+
deactivate_blockbuster()
181+
163182
# Deprecated
164183
if ATTR_WAIT_BOOT in body:
165184
self.sys_config.wait_boot = body[ATTR_WAIT_BOOT]

supervisor/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ATTR_ADDONS_CUSTOM_LIST,
1313
ATTR_DEBUG,
1414
ATTR_DEBUG_BLOCK,
15+
ATTR_DETECT_BLOCKING_IO,
1516
ATTR_DIAGNOSTICS,
1617
ATTR_IMAGE,
1718
ATTR_LAST_BOOT,
@@ -142,6 +143,16 @@ def debug_block(self, value: bool) -> None:
142143
"""Set debug wait mode."""
143144
self._data[ATTR_DEBUG_BLOCK] = value
144145

146+
@property
147+
def detect_blocking_io(self) -> bool:
148+
"""Return True if blocking I/O in event loop detection enabled at startup."""
149+
return self._data[ATTR_DETECT_BLOCKING_IO]
150+
151+
@detect_blocking_io.setter
152+
def detect_blocking_io(self, value: bool) -> None:
153+
"""Enable/Disable blocking I/O in event loop detection at startup."""
154+
self._data[ATTR_DETECT_BLOCKING_IO] = value
155+
145156
@property
146157
def diagnostics(self) -> bool | None:
147158
"""Return bool if diagnostics is set otherwise None."""

supervisor/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
ATTR_DEPLOYMENT = "deployment"
153153
ATTR_DESCRIPTON = "description"
154154
ATTR_DETACHED = "detached"
155+
ATTR_DETECT_BLOCKING_IO = "detect_blocking_io"
155156
ATTR_DEVICES = "devices"
156157
ATTR_DEVICETREE = "devicetree"
157158
ATTR_DIAGNOSTICS = "diagnostics"

supervisor/utils/blockbuster.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Activate and deactivate blockbuster for finding blocking I/O."""
2+
3+
from functools import lru_cache
4+
import logging
5+
6+
from blockbuster import BlockBuster
7+
8+
_LOGGER: logging.Logger = logging.getLogger(__name__)
9+
10+
11+
@lru_cache(maxsize=1)
12+
def _get_blockbuster() -> BlockBuster:
13+
"""Get blockbuster instance."""
14+
return BlockBuster()
15+
16+
17+
def blockbuster_enabled() -> bool:
18+
"""Return true if blockbuster detection is enabled."""
19+
blockbuster = _get_blockbuster()
20+
# We activate all or none so just check the first one
21+
for _, fn in blockbuster.functions.items():
22+
return fn.activated
23+
return False
24+
25+
26+
def activate_blockbuster() -> None:
27+
"""Activate blockbuster detection."""
28+
_LOGGER.info("Activating BlockBuster blocking I/O detection")
29+
_get_blockbuster().activate()
30+
31+
32+
def deactivate_blockbuster() -> None:
33+
"""Deactivate blockbuster detection."""
34+
_LOGGER.info("Deactivating BlockBuster blocking I/O detection")
35+
_get_blockbuster().deactivate()

supervisor/validate.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
ATTR_CONTENT_TRUST,
1616
ATTR_DEBUG,
1717
ATTR_DEBUG_BLOCK,
18+
ATTR_DETECT_BLOCKING_IO,
1819
ATTR_DIAGNOSTICS,
1920
ATTR_DISPLAYNAME,
2021
ATTR_DNS,
@@ -162,6 +163,7 @@ def validate_repository(repository: str) -> str:
162163
vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(),
163164
vol.Optional(ATTR_DEBUG_BLOCK, default=False): vol.Boolean(),
164165
vol.Optional(ATTR_DIAGNOSTICS, default=None): vol.Maybe(vol.Boolean()),
166+
vol.Optional(ATTR_DETECT_BLOCKING_IO, default=False): vol.Boolean(),
165167
},
166168
extra=vol.REMOVE_EXTRA,
167169
)

tests/api/test_supervisor.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Test Supervisor API."""
22

33
# pylint: disable=protected-access
4+
import time
45
from unittest.mock import MagicMock, patch
56

67
from aiohttp.test_utils import TestClient
8+
from blockbuster import BlockingError
79
import pytest
810

911
from supervisor.coresys import CoreSys
@@ -247,3 +249,46 @@ async def test_api_supervisor_options_timezone(
247249
assert resp.status == 200
248250

249251
assert coresys.timezone == "Europe/Zurich"
252+
253+
254+
@pytest.mark.parametrize(
255+
("blockbuster", "option_value", "config_value"),
256+
[("no_blockbuster", "on", False), ("no_blockbuster", "on_at_startup", True)],
257+
indirect=["blockbuster"],
258+
)
259+
async def test_api_supervisor_options_blocking_io(
260+
api_client: TestClient, coresys: CoreSys, option_value: str, config_value: bool
261+
):
262+
"""Test setting supervisor detect blocking io option."""
263+
# This should not fail with a blocking error yet
264+
time.sleep(0)
265+
266+
resp = await api_client.post(
267+
"/supervisor/options", json={"detect_blocking_io": option_value}
268+
)
269+
assert resp.status == 200
270+
271+
resp = await api_client.get("/supervisor/info")
272+
assert resp.status == 200
273+
body = await resp.json()
274+
assert body["data"]["detect_blocking_io"] is True
275+
276+
# This remains false because we only turned it on for current run of supervisor, not permanently
277+
assert coresys.config.detect_blocking_io is config_value
278+
279+
with pytest.raises(BlockingError):
280+
time.sleep(0)
281+
282+
resp = await api_client.post(
283+
"/supervisor/options", json={"detect_blocking_io": "off"}
284+
)
285+
assert resp.status == 200
286+
287+
resp = await api_client.get("/supervisor/info")
288+
assert resp.status == 200
289+
body = await resp.json()
290+
assert body["data"]["detect_blocking_io"] is False
291+
assert coresys.config.detect_blocking_io is False
292+
293+
# This should not raise blocking error anymore
294+
time.sleep(0)

tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@
6565

6666

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

0 commit comments

Comments
 (0)