Home Assistant Unofficial Reference 2024.12.1
loop.py
Go to the documentation of this file.
1 """asyncio loop utilities."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import functools
7 from functools import cache
8 import linecache
9 import logging
10 import threading
11 import traceback
12 from typing import Any
13 
14 from homeassistant.core import async_get_hass_or_none
15 from homeassistant.helpers.frame import (
16  MissingIntegrationFrame,
17  get_current_frame,
18  get_integration_frame,
19 )
20 from homeassistant.loader import async_suggest_report_issue
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 
25 def _get_line_from_cache(filename: str, lineno: int) -> str:
26  """Get line from cache or read from file."""
27  return (linecache.getline(filename, lineno) or "?").strip()
28 
29 
30 # Set of previously reported blocking calls
31 # (integration, filename, lineno)
32 _PREVIOUSLY_REPORTED: set[tuple[str | None, str, int | Any]] = set()
33 
34 
36  func: Callable[..., Any],
37  check_allowed: Callable[[dict[str, Any]], bool] | None = None,
38  strict: bool = True,
39  strict_core: bool = True,
40  **mapped_args: Any,
41 ) -> None:
42  """Warn if called inside the event loop. Raise if `strict` is True."""
43  if check_allowed is not None and check_allowed(mapped_args):
44  return
45 
46  found_frame = None
47  offender_frame = get_current_frame(2)
48  offender_filename = offender_frame.f_code.co_filename
49  offender_lineno = offender_frame.f_lineno
50  offender_line = _get_line_from_cache(offender_filename, offender_lineno)
51  report_key: tuple[str | None, str, int | Any]
52 
53  try:
54  integration_frame = get_integration_frame()
55  except MissingIntegrationFrame:
56  # Did not source from integration? Hard error.
57  report_key = (None, offender_filename, offender_lineno)
58  was_reported = report_key in _PREVIOUSLY_REPORTED
59  _PREVIOUSLY_REPORTED.add(report_key)
60  if not strict_core:
61  if was_reported:
62  _LOGGER.debug(
63  "Detected blocking call to %s with args %s in %s, "
64  "line %s: %s inside the event loop; "
65  "This is causing stability issues. "
66  "Please create a bug report at "
67  "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n"
68  "%s\n",
69  func.__name__,
70  mapped_args.get("args"),
71  offender_filename,
72  offender_lineno,
73  offender_line,
74  _dev_help_message(func.__name__),
75  )
76  else:
77  _LOGGER.warning(
78  "Detected blocking call to %s with args %s in %s, "
79  "line %s: %s inside the event loop; "
80  "This is causing stability issues. "
81  "Please create a bug report at "
82  "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n"
83  "%s\n"
84  "Traceback (most recent call last):\n%s",
85  func.__name__,
86  mapped_args.get("args"),
87  offender_filename,
88  offender_lineno,
89  offender_line,
90  _dev_help_message(func.__name__),
91  "".join(traceback.format_stack(f=offender_frame)),
92  )
93  return
94 
95  if found_frame is None:
96  raise RuntimeError( # noqa: TRY200
97  f"Caught blocking call to {func.__name__} with args {mapped_args.get("args")} "
98  f"in {offender_filename}, line {offender_lineno}: {offender_line} "
99  "inside the event loop; "
100  "This is causing stability issues. "
101  "Please create a bug report at "
102  "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue\n"
103  f"{_dev_help_message(func.__name__)}"
104  )
105 
106  report_key = (integration_frame.integration, offender_filename, offender_lineno)
107  was_reported = report_key in _PREVIOUSLY_REPORTED
108  _PREVIOUSLY_REPORTED.add(report_key)
109 
110  report_issue = async_suggest_report_issue(
112  integration_domain=integration_frame.integration,
113  module=integration_frame.module,
114  )
115 
116  if was_reported:
117  _LOGGER.debug(
118  "Detected blocking call to %s with args %s "
119  "inside the event loop by %sintegration '%s' "
120  "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n"
121  "%s\n",
122  func.__name__,
123  mapped_args.get("args"),
124  "custom " if integration_frame.custom_integration else "",
125  integration_frame.integration,
126  integration_frame.relative_filename,
127  integration_frame.line_number,
128  integration_frame.line,
129  offender_filename,
130  offender_lineno,
131  offender_line,
132  report_issue,
133  _dev_help_message(func.__name__),
134  )
135  else:
136  _LOGGER.warning(
137  "Detected blocking call to %s with args %s "
138  "inside the event loop by %sintegration '%s' "
139  "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n"
140  "%s\n"
141  "Traceback (most recent call last):\n%s",
142  func.__name__,
143  mapped_args.get("args"),
144  "custom " if integration_frame.custom_integration else "",
145  integration_frame.integration,
146  integration_frame.relative_filename,
147  integration_frame.line_number,
148  integration_frame.line,
149  offender_filename,
150  offender_lineno,
151  offender_line,
152  report_issue,
153  _dev_help_message(func.__name__),
154  "".join(traceback.format_stack(f=integration_frame.frame)),
155  )
156 
157  if strict:
158  raise RuntimeError(
159  f"Caught blocking call to {func.__name__} with args "
160  f"{mapped_args.get('args')} inside the event loop by "
161  f"{'custom ' if integration_frame.custom_integration else ''}"
162  f"integration '{integration_frame.integration}' at "
163  f"{integration_frame.relative_filename}, line {integration_frame.line_number}:"
164  f" {integration_frame.line}. (offender: {offender_filename}, line "
165  f"{offender_lineno}: {offender_line}), please {report_issue}\n"
166  f"{_dev_help_message(func.__name__)}"
167  )
168 
169 
170 @cache
171 def _dev_help_message(what: str) -> str:
172  """Generate help message to guide developers."""
173  return (
174  "For developers, please see "
175  "https://developers.home-assistant.io/docs/asyncio_blocking_operations/"
176  f"#{what.replace('.', '')}"
177  )
178 
179 
180 def protect_loop[**_P, _R](
181  func: Callable[_P, _R],
182  loop_thread_id: int,
183  strict: bool = True,
184  strict_core: bool = True,
185  check_allowed: Callable[[dict[str, Any]], bool] | None = None,
186 ) -> Callable[_P, _R]:
187  """Protect function from running in event loop."""
188 
189  @functools.wraps(func)
190  def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R:
191  if threading.get_ident() == loop_thread_id:
193  func,
194  strict=strict,
195  strict_core=strict_core,
196  check_allowed=check_allowed,
197  args=args,
198  kwargs=kwargs,
199  )
200  return func(*args, **kwargs)
201 
202  return protected_loop_func
HomeAssistant|None async_get_hass_or_none()
Definition: core.py:299
FrameType get_current_frame(int depth=0)
Definition: frame.py:78
IntegrationFrame get_integration_frame(set|None exclude_integrations=None)
Definition: frame.py:84
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752
str _get_line_from_cache(str filename, int lineno)
Definition: loop.py:25
None raise_for_blocking_call(Callable[..., Any] func, Callable[[dict[str, Any]], bool]|None check_allowed=None, bool strict=True, bool strict_core=True, **Any mapped_args)
Definition: loop.py:41
str _dev_help_message(str what)
Definition: loop.py:171