mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 20:56:18 +02:00
bambuddy.log on Windows fills with
Exception in callback _ProactorBasePipeTransport._call_connection_lost()
ConnectionResetError: [WinError 10054] An existing connection was
forcibly closed by the remote host
every time a printer / MQTT broker / camera RSTs a TCP socket instead
of FINing it. The application-layer reconnect (paho-mqtt, httpx)
handles the actual disconnect fine; the traceback is asyncio
bookkeeping. Reported by @cadtoolbox who runs 9 printers including 5
offline X1Es, so the log filled multiple times per minute.
New backend/app/core/asyncio_handlers.py installs a custom
loop.set_exception_handler on Windows that pattern-matches three
signals together (platform == win32, exception is
ConnectionResetError, asyncio message contains
_call_connection_lost) and demotes the entry to DEBUG. Genuine
ConnectionResetErrors raised inside application coroutines have a
different message string and still surface; BrokenPipeError /
ConnectionAbortedError on the same cleanup path also still surface.
Wired from lifespan startup before any task can spawn that might
trip it. Linux / macOS use the Selector loop, so install is an
explicit no-op there with a False return.
9 unit tests in test_asyncio_handlers.py covering signature match,
rejection of unrelated resets, platform gate, suppress vs.
pass-through to default handler.
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""Asyncio event-loop exception handlers used at app startup.
|
|
|
|
Currently houses a single Windows-specific filter for the noisy
|
|
``_ProactorBasePipeTransport._call_connection_lost`` ``WinError 10054``
|
|
that fires every time a printer / MQTT broker / camera RSTs a TCP socket
|
|
instead of closing it cleanly. See ``install_proactor_reset_filter`` for
|
|
the why and the failure mode it suppresses.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _is_proactor_connection_reset(context: dict[str, Any]) -> bool:
|
|
"""True if `context` describes the Windows Proactor cleanup-RST noise.
|
|
|
|
asyncio's default exception handler is invoked in two distinct cases
|
|
we care about — generic uncaught task exceptions, and the specific
|
|
`_call_connection_lost` cleanup path — and we only want to suppress
|
|
the latter. Match on three signals together so a real
|
|
`ConnectionResetError` raised inside an application task still
|
|
surfaces normally:
|
|
|
|
1. The exception is `ConnectionResetError` (or a subclass).
|
|
2. asyncio's own message string mentions `_call_connection_lost`
|
|
(the Proactor-cleanup callback is the only place Python emits
|
|
this exact phrase).
|
|
3. We're actually on Windows, where the Proactor is in use.
|
|
"""
|
|
if sys.platform != "win32":
|
|
return False
|
|
exc = context.get("exception")
|
|
if not isinstance(exc, ConnectionResetError):
|
|
return False
|
|
message = context.get("message", "")
|
|
return "_call_connection_lost" in message
|
|
|
|
|
|
def _proactor_reset_filter(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
|
|
"""Custom event-loop exception handler.
|
|
|
|
Handles the Proactor-cleanup `ConnectionResetError` by logging it at
|
|
DEBUG instead of ERROR, and delegates everything else to asyncio's
|
|
default handler so unrelated bugs are still visible.
|
|
"""
|
|
if _is_proactor_connection_reset(context):
|
|
logger.debug(
|
|
"asyncio Proactor: peer reset socket during cleanup (WinError 10054); "
|
|
"ignored — application-layer reconnect handles the disconnect"
|
|
)
|
|
return
|
|
loop.default_exception_handler(context)
|
|
|
|
|
|
def install_proactor_reset_filter(loop: asyncio.AbstractEventLoop | None = None) -> bool:
|
|
"""Install the filter on `loop` (or the running loop if omitted).
|
|
|
|
Returns True when the filter was installed (Windows only), False on
|
|
every other platform — so callers can branch on the return value if
|
|
they want to log the install / skip.
|
|
"""
|
|
if sys.platform != "win32":
|
|
return False
|
|
if loop is None:
|
|
loop = asyncio.get_running_loop()
|
|
loop.set_exception_handler(_proactor_reset_filter)
|
|
return True
|