Blocking operations
How to keep blocking I/O and slow imports out of the event loop, and how to fix the blocking calls Home Assistant reports.
A blocking call in the event loop stalls all of Home Assistant until it returns. Network I/O, disk I/O, sleeping, and heavy computation all block. Do them in the executor, or use an async alternative.
Home Assistant detects many blocking calls in the event loop and reports them in the log, with the integration and the line that made the call. Two of them, sleep and putrequest, are also stopped: the call raises a RuntimeError instead of running. If you got here from such a report, find your call in the list below.
In Home Assistant, use hass.async_add_executor_job. In a library, use the standard loop.run_in_executor:
import asyncio
from functools import partial
# In Home Assistant
result = await hass.async_add_executor_job(blocking_code, "something")
result = await hass.async_add_executor_job(
partial(blocking_code_with_kwargs, kwarg=True)
)
# In library code
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, blocking_code, "something")Read Python's notes on running blocking code for the pitfalls.
open, glob.glob, glob.iglob, os.walk, os.listdir, os.scandir, and the pathlib.Path methods like read_text, read_bytes, write_text, and write_bytes all do disk I/O. Run them in the executor. The same goes for calls Home Assistant does not detect, like os.stat or os.path.exists.
Home Assistant only detects the open call, not the reads and writes on the file after it. When you move open into the executor, move the reads and writes along with it. A fix that only moves open leaves blocking I/O in place that nobody will notice for a long time.
Replace time.sleep with await asyncio.sleep. A common source of a blocking sleep is pyserial-asyncio. Use serialx instead: it has native asyncio support, and is import-compatible with serial, serial_asyncio, and serial_asyncio_fast. See migrating from pyserial to serialx.
http.client and anything built on it, like urllib and requests, does blocking network I/O. Run it in the executor, or better, switch to an async HTTP client like aiohttp.
Creating an SSL context reads certificates from disk. Do not create your own: use the shared clients and contexts Home Assistant already prepared outside the event loop.
- For
aiohttp:async_get_clientsession(hass)fromhomeassistant.helpers.aiohttp_client. - For
httpx2:get_async_client(hass)fromhomeassistant.helpers.httpx_client. - For anything else: the helpers in
homeassistant.util.ssl, likeget_default_context().
Importing a module reads it from disk. See imports below.
Imports need care for two reasons: an import reads files from disk, which blocks, and imports in CPython are not thread safe.
If your integration imports what it needs at the top of its modules, and __init__.py imports the modules it needs, you have nothing to worry about. Home Assistant loads your integration before the event loop starts, or in the import executor: a separate thread just for imports.
This is the right choice for almost every integration.
Sometimes a module is only needed in a rare case, or a top-level import creates a circular import. Then:
- If the module is already loaded, a plain
importstatement is fine: Python does not load it again. - If the module is only imported in one place, put the import in a function and run that with
hass.async_add_executor_job. - If the module may be imported from different places at the same time, use
async_import_modulefromhomeassistant.helpers.importlib. It caches the result and makes concurrent imports wait for one another.
from homeassistant.helpers.importlib import async_import_module
module = await async_import_module(
hass, f"homeassistant.components.my_integration.{name}"
)To check whether a module is loaded, look in sys.modules. A module appears there as soon as loading starts, so with imports from several paths, keep the race conditions in mind.
If you only need an import for type hints, guard it, so it is not imported at runtime at all:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from some_library import SomeClass
def some_function() -> SomeClass:
...Importing costs CPU time and disk I/O, and memory once it is loaded. For code that is only needed now and then, a late import can be the better trade, as long as it follows the rules above.
Turn on debug mode while you develop, with debug: true under homeassistant: in configuration.yaml, and run with Python's asyncio debug mode. Both catch blocking calls that otherwise only show up as a slow system.