Patch http.client to not do I/O in the event loop (#34194)
This commit is contained in:
parent
5bfc1f3d4d
commit
d011b46985
4 changed files with 163 additions and 3 deletions
14
homeassistant/block_async_io.py
Normal file
14
homeassistant/block_async_io.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""Block I/O being done in asyncio."""
|
||||||
|
from http.client import HTTPConnection
|
||||||
|
|
||||||
|
from homeassistant.util.async_ import protect_loop
|
||||||
|
|
||||||
|
|
||||||
|
def enable() -> None:
|
||||||
|
"""Enable the detection of I/O in the event loop."""
|
||||||
|
# Prevent urllib3 and requests doing I/O in event loop
|
||||||
|
HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest)
|
||||||
|
|
||||||
|
# Currently disabled. pytz doing I/O when getting timezone.
|
||||||
|
# Prevent files being opened inside the event loop
|
||||||
|
# builtins.open = protect_loop(builtins.open)
|
|
@ -36,7 +36,7 @@ from async_timeout import timeout
|
||||||
import attr
|
import attr
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import loader, util
|
from homeassistant import block_async_io, loader, util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DOMAIN,
|
ATTR_DOMAIN,
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
|
@ -77,6 +77,9 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.config_entries import ConfigEntries
|
from homeassistant.config_entries import ConfigEntries
|
||||||
from homeassistant.components.http import HomeAssistantHTTP
|
from homeassistant.components.http import HomeAssistantHTTP
|
||||||
|
|
||||||
|
|
||||||
|
block_async_io.enable()
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)
|
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Asyncio backports for Python 3.6 compatibility."""
|
"""Asyncio backports for Python 3.6 compatibility."""
|
||||||
from asyncio import coroutines, ensure_future
|
from asyncio import coroutines, ensure_future, get_running_loop
|
||||||
from asyncio.events import AbstractEventLoop
|
from asyncio.events import AbstractEventLoop
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
from traceback import extract_stack
|
||||||
from typing import Any, Callable, Coroutine
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -55,3 +57,65 @@ def run_callback_threadsafe(
|
||||||
|
|
||||||
loop.call_soon_threadsafe(run_callback)
|
loop.call_soon_threadsafe(run_callback)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
|
|
||||||
|
def check_loop() -> None:
|
||||||
|
"""Warn if called inside the event loop."""
|
||||||
|
try:
|
||||||
|
get_running_loop()
|
||||||
|
in_loop = True
|
||||||
|
except RuntimeError:
|
||||||
|
in_loop = False
|
||||||
|
|
||||||
|
if not in_loop:
|
||||||
|
return
|
||||||
|
|
||||||
|
found_frame = None
|
||||||
|
|
||||||
|
for frame in reversed(extract_stack()):
|
||||||
|
for path in ("custom_components/", "homeassistant/components/"):
|
||||||
|
try:
|
||||||
|
index = frame.filename.index(path)
|
||||||
|
found_frame = frame
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if found_frame is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Did not source from integration? Hard error.
|
||||||
|
if found_frame is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Detected I/O inside the event loop. This is causing stability issues. Please report issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
start = index + len(path)
|
||||||
|
end = found_frame.filename.index("/", start)
|
||||||
|
|
||||||
|
integration = found_frame.filename[start:end]
|
||||||
|
|
||||||
|
if path == "custom_components/":
|
||||||
|
extra = " to the custom component author"
|
||||||
|
else:
|
||||||
|
extra = ""
|
||||||
|
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Detected I/O inside the event loop. This is causing stability issues. Please report issue%s for %s doing I/O at %s, line %s: %s",
|
||||||
|
extra,
|
||||||
|
integration,
|
||||||
|
found_frame.filename[index:],
|
||||||
|
found_frame.lineno,
|
||||||
|
found_frame.line.strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def protect_loop(func: Callable) -> Callable:
|
||||||
|
"""Protect function from running in event loop."""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def protected_loop_func(*args, **kwargs): # type: ignore
|
||||||
|
check_loop()
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return protected_loop_func
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Tests for async util methods from Python source."""
|
"""Tests for async util methods from Python source."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -165,3 +165,82 @@ class RunThreadsafeTests(TestCase):
|
||||||
with self.assertRaises(ValueError) as exc_context:
|
with self.assertRaises(ValueError) as exc_context:
|
||||||
self.loop.run_until_complete(future)
|
self.loop.run_until_complete(future)
|
||||||
self.assertIn("Invalid!", exc_context.exception.args)
|
self.assertIn("Invalid!", exc_context.exception.args)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_loop_async():
|
||||||
|
"""Test check_loop detects when called from event loop without integration context."""
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
hasync.check_loop()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_loop_async_integration(caplog):
|
||||||
|
"""Test check_loop detects when called from event loop from integration context."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.util.async_.extract_stack",
|
||||||
|
return_value=[
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/homeassistant/core.py",
|
||||||
|
lineno="23",
|
||||||
|
line="do_something()",
|
||||||
|
),
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/homeassistant/components/hue/light.py",
|
||||||
|
lineno="23",
|
||||||
|
line="self.light.is_on",
|
||||||
|
),
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/aiohue/lights.py",
|
||||||
|
lineno="2",
|
||||||
|
line="something()",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
):
|
||||||
|
hasync.check_loop()
|
||||||
|
assert (
|
||||||
|
"Detected I/O inside the event loop. This is causing stability issues. Please report issue for hue doing I/O at homeassistant/components/hue/light.py, line 23: self.light.is_on"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_loop_async_custom(caplog):
|
||||||
|
"""Test check_loop detects when called from event loop with custom component context."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.util.async_.extract_stack",
|
||||||
|
return_value=[
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/homeassistant/core.py",
|
||||||
|
lineno="23",
|
||||||
|
line="do_something()",
|
||||||
|
),
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/config/custom_components/hue/light.py",
|
||||||
|
lineno="23",
|
||||||
|
line="self.light.is_on",
|
||||||
|
),
|
||||||
|
Mock(
|
||||||
|
filename="/home/paulus/aiohue/lights.py",
|
||||||
|
lineno="2",
|
||||||
|
line="something()",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
):
|
||||||
|
hasync.check_loop()
|
||||||
|
assert (
|
||||||
|
"Detected I/O inside the event loop. This is causing stability issues. Please report issue to the custom component author for hue doing I/O at custom_components/hue/light.py, line 23: self.light.is_on"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_loop_sync(caplog):
|
||||||
|
"""Test check_loop does nothing when called from thread."""
|
||||||
|
hasync.check_loop()
|
||||||
|
assert "Detected I/O inside the event loop" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_protect_loop_sync():
|
||||||
|
"""Test protect_loop calls check_loop."""
|
||||||
|
calls = []
|
||||||
|
with patch("homeassistant.util.async_.check_loop") as mock_loop:
|
||||||
|
hasync.protect_loop(calls.append)(1)
|
||||||
|
assert len(mock_loop.mock_calls) == 1
|
||||||
|
assert calls == [1]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue