Home Assistant Unofficial Reference 2024.12.1
block_async_io.py
Go to the documentation of this file.
1 """Block blocking calls being done in asyncio."""
2 
3 import builtins
4 from collections.abc import Callable
5 from contextlib import suppress
6 from dataclasses import dataclass
7 import glob
8 from http.client import HTTPConnection
9 import importlib
10 import os
11 from pathlib import Path
12 from ssl import SSLContext
13 import sys
14 import threading
15 import time
16 from typing import Any
17 
18 from .helpers.frame import get_current_frame
19 from .util.loop import protect_loop
20 
21 _IN_TESTS = "unittest" in sys.modules
22 
23 ALLOWED_FILE_PREFIXES = ("/proc",)
24 
25 
26 def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
27  # If the module is already imported, we can ignore it.
28  return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
29 
30 
31 def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
32  # If the file is in /proc we can ignore it.
33  args = mapped_args["args"]
34  path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
35  return path.startswith(ALLOWED_FILE_PREFIXES)
36 
37 
38 def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
39  #
40  # Avoid extracting the stack unless we need to since it
41  # will have to access the linecache which can do blocking
42  # I/O and we are trying to avoid blocking calls.
43  #
44  # frame[0] is us
45  # frame[1] is raise_for_blocking_call
46  # frame[2] is protected_loop_func
47  # frame[3] is the offender
48  with suppress(ValueError):
49  return get_current_frame(4).f_code.co_filename.endswith("pydevd.py")
50  return False
51 
52 
53 @dataclass(slots=True, frozen=True)
55  """Class to hold information about a blocking call."""
56 
57  original_func: Callable
58  object: object
59  function: str
60  check_allowed: Callable[[dict[str, Any]], bool] | None
61  strict: bool
62  strict_core: bool
63  skip_for_tests: bool
64 
65 
66 _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
68  original_func=HTTPConnection.putrequest,
69  object=HTTPConnection,
70  function="putrequest",
71  check_allowed=None,
72  strict=True,
73  strict_core=True,
74  skip_for_tests=False,
75  ),
77  original_func=time.sleep,
78  object=time,
79  function="sleep",
80  check_allowed=_check_sleep_call_allowed,
81  strict=True,
82  strict_core=True,
83  skip_for_tests=False,
84  ),
86  original_func=glob.glob,
87  object=glob,
88  function="glob",
89  check_allowed=None,
90  strict=False,
91  strict_core=False,
92  skip_for_tests=False,
93  ),
95  original_func=glob.iglob,
96  object=glob,
97  function="iglob",
98  check_allowed=None,
99  strict=False,
100  strict_core=False,
101  skip_for_tests=False,
102  ),
103  BlockingCall(
104  original_func=os.walk,
105  object=os,
106  function="walk",
107  check_allowed=None,
108  strict=False,
109  strict_core=False,
110  skip_for_tests=False,
111  ),
112  BlockingCall(
113  original_func=os.listdir,
114  object=os,
115  function="listdir",
116  check_allowed=None,
117  strict=False,
118  strict_core=False,
119  skip_for_tests=True,
120  ),
121  BlockingCall(
122  original_func=os.scandir,
123  object=os,
124  function="scandir",
125  check_allowed=None,
126  strict=False,
127  strict_core=False,
128  skip_for_tests=True,
129  ),
130  BlockingCall(
131  original_func=builtins.open,
132  object=builtins,
133  function="open",
134  check_allowed=_check_file_allowed,
135  strict=False,
136  strict_core=False,
137  skip_for_tests=True,
138  ),
139  BlockingCall(
140  original_func=importlib.import_module,
141  object=importlib,
142  function="import_module",
143  check_allowed=_check_import_call_allowed,
144  strict=False,
145  strict_core=False,
146  skip_for_tests=True,
147  ),
148  BlockingCall(
149  original_func=SSLContext.load_default_certs,
150  object=SSLContext,
151  function="load_default_certs",
152  check_allowed=None,
153  strict=False,
154  strict_core=False,
155  skip_for_tests=True,
156  ),
157  BlockingCall(
158  original_func=SSLContext.load_verify_locations,
159  object=SSLContext,
160  function="load_verify_locations",
161  check_allowed=None,
162  strict=False,
163  strict_core=False,
164  skip_for_tests=True,
165  ),
166  BlockingCall(
167  original_func=SSLContext.load_cert_chain,
168  object=SSLContext,
169  function="load_cert_chain",
170  check_allowed=None,
171  strict=False,
172  strict_core=False,
173  skip_for_tests=True,
174  ),
175  BlockingCall(
176  original_func=Path.open,
177  object=Path,
178  function="open",
179  check_allowed=_check_file_allowed,
180  strict=False,
181  strict_core=False,
182  skip_for_tests=True,
183  ),
184  BlockingCall(
185  original_func=Path.read_text,
186  object=Path,
187  function="read_text",
188  check_allowed=_check_file_allowed,
189  strict=False,
190  strict_core=False,
191  skip_for_tests=True,
192  ),
193  BlockingCall(
194  original_func=Path.read_bytes,
195  object=Path,
196  function="read_bytes",
197  check_allowed=_check_file_allowed,
198  strict=False,
199  strict_core=False,
200  skip_for_tests=True,
201  ),
202  BlockingCall(
203  original_func=Path.write_text,
204  object=Path,
205  function="write_text",
206  check_allowed=_check_file_allowed,
207  strict=False,
208  strict_core=False,
209  skip_for_tests=True,
210  ),
211  BlockingCall(
212  original_func=Path.write_bytes,
213  object=Path,
214  function="write_bytes",
215  check_allowed=_check_file_allowed,
216  strict=False,
217  strict_core=False,
218  skip_for_tests=True,
219  ),
220 )
221 
222 
223 @dataclass(slots=True)
225  """Class to track which calls are blocked."""
226 
227  calls: set[BlockingCall]
228 
229 
230 _BLOCKED_CALLS = BlockedCalls(set())
231 
232 
233 def enable() -> None:
234  """Enable the detection of blocking calls in the event loop."""
235  calls = _BLOCKED_CALLS.calls
236  if calls:
237  raise RuntimeError("Blocking call detection is already enabled")
238 
239  loop_thread_id = threading.get_ident()
240  for blocking_call in _BLOCKING_CALLS:
241  if _IN_TESTS and blocking_call.skip_for_tests:
242  continue
243 
244  protected_function = protect_loop(
245  blocking_call.original_func,
246  strict=blocking_call.strict,
247  strict_core=blocking_call.strict_core,
248  check_allowed=blocking_call.check_allowed,
249  loop_thread_id=loop_thread_id,
250  )
251  setattr(blocking_call.object, blocking_call.function, protected_function)
252  calls.add(blocking_call)
bool _check_sleep_call_allowed(dict[str, Any] mapped_args)
bool _check_file_allowed(dict[str, Any] mapped_args)
bool _check_import_call_allowed(dict[str, Any] mapped_args)
FrameType get_current_frame(int depth=0)
Definition: frame.py:78