From f527fd0947aa0b8d30374adbe8ec7b6f17db92af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 11:11:22 -1000 Subject: [PATCH] Improve error reporting when an integration tries to create a task in a thread (#115307) --- homeassistant/util/async_.py | 20 +++++++--- tests/util/test_async.py | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 5ca19296b41..0cf9fc992c5 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -24,12 +24,20 @@ def create_eager_task( loop: AbstractEventLoop | None = None, ) -> Task[_T]: """Create a task from a coroutine and schedule it to run immediately.""" - return Task( - coro, - loop=loop or get_running_loop(), - name=name, - eager_start=True, - ) + if not loop: + try: + loop = get_running_loop() + except RuntimeError: + # If there is no running loop, create_eager_task is being called from + # the wrong thread. + # Late import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.helpers import frame + + frame.report("attempted to create an asyncio task from a thread") + raise + + return Task(coro, loop=loop, name=name, eager_start=True) def cancelling(task: Future[Any]) -> bool: diff --git a/tests/util/test_async.py b/tests/util/test_async.py index d0131df88ee..157becc4b01 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,6 +9,8 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.util import async_ as hasync +from tests.common import extract_stack_to_frame + @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -123,3 +125,73 @@ async def test_create_eager_task_312(hass: HomeAssistant) -> None: assert events == ["eager", "normal"] await task1 await task2 + + +async def test_create_eager_task_from_thread(hass: HomeAssistant) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + with pytest.raises( + RuntimeError, + match=( + "Detected code that attempted to create an asyncio task from a thread. Please report this issue." + ), + ): + await hass.async_add_executor_job(create_task) + + +async def test_create_eager_task_from_thread_in_integration( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + frames = extract_stack_to_frame( + [ + 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()", + ), + ] + ) + with ( + pytest.raises(RuntimeError, match="no running event loop"), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + await hass.async_add_executor_job(create_task) + + assert ( + "Detected that integration 'hue' attempted to create an asyncio task " + "from a thread at homeassistant/components/hue/light.py, line 23: " + "self.light.is_on" + ) in caplog.text