feat: detect event loop blocking in development

HA
5h ago
+631-5

Summary

Warn in development when an event blocks the asyncio event loop.

  • A watchdog thread puts a probe on the loop every 1–10 ms. If the probe does not run within event_loop_blocking_threshold seconds (default 0.2), the loop is blocked.
  • While the loop is still blocked, the monitor reads asyncio.current_task(loop) and gets the handler from the event task's name (reflex_event|<event>|…). This covers the whole event, including computed vars recalculated after the handler.
  • The first warning appears at once, so a handler that never returns is reported too. It names the handler and the innermost line of app code, then shows the stack of the event task. A second line gives the total duration when the loop responds.
  • Detection is on by default in development and always off in production. Set event_loop_blocking_threshold=0, or REFLEX_EVENT_LOOP_BLOCKING_THRESHOLD=0, to turn it off.
  • There is no cost on the event path. The monitor uses about 1% of one core while idle.

Example:

Event handler State.http_sync is blocking the event loop at my_app/my_app.py:115 in http_sync (threshold: 0.200s), including state updates such as computed vars. Move blocking I/O to a thread pool or use a non-blocking async API.
Blocking stack (most recent call last):
  ...
  File ".../my_app/my_app.py", line 115, in http_sync
    urllib.request.urlopen(SLOW_URL).read()
  ...
  File ".../socket.py", line 729, in readinto
    return self._sock.recv_into(b)
Event handler State.http_sync blocked the event loop for 0.501s in total.

Closes #7619

Testing

  • Unit tests for the monitor, the event processor, the config and the app wiring. The tests cover sync, async and post-handler blocking, a hung handler, a stale frame snapshot, threshold 0, and the env var.
  • Dev server with 11 blocking workloads: sync sleep, CPU loop, sqlite, pandas, sync HTTP, a blocking async handler, a background event, a helper function and a slow computed var. All 11 are detected with the correct handler and app line, on Python 3.11 and 3.14, with asyncio, uvloop and rloop. There were no warnings for awaiting handlers or blocks shorter than the threshold.
  • The durations agree with py-spy within 30 ms.
  • Event throughput with the monitor on is 97.6–99.1% of the throughput with it off.