asyncio_handlers.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. """Asyncio event-loop exception handlers used at app startup.
  2. Currently houses a single Windows-specific filter for the noisy
  3. ``_ProactorBasePipeTransport._call_connection_lost`` ``WinError 10054``
  4. that fires every time a printer / MQTT broker / camera RSTs a TCP socket
  5. instead of closing it cleanly. See ``install_proactor_reset_filter`` for
  6. the why and the failure mode it suppresses.
  7. """
  8. from __future__ import annotations
  9. import asyncio
  10. import logging
  11. import sys
  12. from typing import Any
  13. logger = logging.getLogger(__name__)
  14. def _is_proactor_connection_reset(context: dict[str, Any]) -> bool:
  15. """True if `context` describes the Windows Proactor cleanup-RST noise.
  16. asyncio's default exception handler is invoked in two distinct cases
  17. we care about — generic uncaught task exceptions, and the specific
  18. `_call_connection_lost` cleanup path — and we only want to suppress
  19. the latter. Match on three signals together so a real
  20. `ConnectionResetError` raised inside an application task still
  21. surfaces normally:
  22. 1. The exception is `ConnectionResetError` (or a subclass).
  23. 2. asyncio's own message string mentions `_call_connection_lost`
  24. (the Proactor-cleanup callback is the only place Python emits
  25. this exact phrase).
  26. 3. We're actually on Windows, where the Proactor is in use.
  27. """
  28. if sys.platform != "win32":
  29. return False
  30. exc = context.get("exception")
  31. if not isinstance(exc, ConnectionResetError):
  32. return False
  33. message = context.get("message", "")
  34. return "_call_connection_lost" in message
  35. def _proactor_reset_filter(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
  36. """Custom event-loop exception handler.
  37. Handles the Proactor-cleanup `ConnectionResetError` by logging it at
  38. DEBUG instead of ERROR, and delegates everything else to asyncio's
  39. default handler so unrelated bugs are still visible.
  40. """
  41. if _is_proactor_connection_reset(context):
  42. logger.debug(
  43. "asyncio Proactor: peer reset socket during cleanup (WinError 10054); "
  44. "ignored — application-layer reconnect handles the disconnect"
  45. )
  46. return
  47. loop.default_exception_handler(context)
  48. def install_proactor_reset_filter(loop: asyncio.AbstractEventLoop | None = None) -> bool:
  49. """Install the filter on `loop` (or the running loop if omitted).
  50. Returns True when the filter was installed (Windows only), False on
  51. every other platform — so callers can branch on the return value if
  52. they want to log the install / skip.
  53. """
  54. if sys.platform != "win32":
  55. return False
  56. if loop is None:
  57. loop = asyncio.get_running_loop()
  58. loop.set_exception_handler(_proactor_reset_filter)
  59. return True