ai-voicebot/voicebot/debug_aioice.py

111 lines
4.9 KiB
Python

import time
from logger import logger
# Defensive monkeypatch: aioice Transaction.__retry may run after the
# underlying datagram transport or loop was torn down which results in
# AttributeError being raised and flooding logs. Wrap the original
# implementation to catch and suppress AttributeError while preserving
# other exceptions. This is a temporary mitigation to keep logs readable
# while we investigate/upstream a proper fix or upgrade aioice.
try:
import aioice.stun as _aioice_stun # type: ignore
# The method is defined with a double-underscore name (__retry) which
# gets name-mangled. Detect the actual attribute name robustly.
retry_attr_name = None
for name in dir(_aioice_stun.Transaction):
if name.endswith("retry"):
obj = getattr(_aioice_stun.Transaction, name)
if callable(obj):
retry_attr_name = name
_orig_retry = obj
break
if retry_attr_name is not None:
# Simple in-process dedupe cache so we only log the same AttributeError
# once per interval. This prevents flooding the logs when many
# transactions race to run after shutdown.
_MONKEYPATCH_LOG_CACHE: dict[str, float] = {}
_MONKEYPATCH_LOG_SUPPRESSION_INTERVAL = 5.0
def _should_log_once(key: str) -> bool:
now = time.time()
last = _MONKEYPATCH_LOG_CACHE.get(key)
if last is None or (now - last) > _MONKEYPATCH_LOG_SUPPRESSION_INTERVAL:
_MONKEYPATCH_LOG_CACHE[key] = now
return True
return False
def _safe_transaction_retry(self, *args, **kwargs): # type: ignore
try:
return _orig_retry(self, *args, **kwargs) # type: ignore
except AttributeError as e: # type: ignore
# Transport or event-loop already closed; log once per key
key = f"Transaction.{retry_attr_name}:{e}"
if _should_log_once(key):
logger.warning(
"aioice Transaction.%s AttributeError suppressed: %s",
retry_attr_name,
e,
)
except Exception: # type: ignore
# Preserve visibility for other unexpected exceptions
logger.exception(
"aioice Transaction.%s raised an unexpected exception",
retry_attr_name,
)
setattr(_aioice_stun.Transaction, retry_attr_name, _safe_transaction_retry) # type: ignore
logger.info("Applied safe aioice Transaction.%s monkeypatch", retry_attr_name)
else:
logger.warning("aioice Transaction.__retry not found; skipping monkeypatch")
except Exception as e:
logger.exception("Failed to apply aioice Transaction.__retry monkeypatch: %s", e)
# Additional defensive patch: wrap the protocol-level send_stun implementation
# (e.g. StunProtocol.send_stun) which ultimately calls the datagram transport's
# sendto. If the transport or its loop is already torn down, sendto can raise
# AttributeError which then triggers asyncio's fatal error path (calling a None
# loop). Wrapping here prevents the flood of selector_events/_fatal_error
# AttributeError traces.
try:
import aioice.ice as _aioice_ice # type: ignore
# Prefer to patch StunProtocol.send_stun which is used by the ICE code.
send_attr_name = None
if hasattr(_aioice_ice, "StunProtocol"):
proto_cls = getattr(_aioice_ice, "StunProtocol")
for name in dir(proto_cls):
if name.endswith("send_stun"):
attr = getattr(proto_cls, name)
if callable(attr):
send_attr_name = name
_orig_send_stun = attr
break
if send_attr_name is not None:
def _safe_send_stun(self, message, addr): # type: ignore
try:
return _orig_send_stun(self, message, addr) # type: ignore
except AttributeError as e: # type: ignore
# Likely transport._sock or transport._loop is None; log once
key = f"StunProtocol.{send_attr_name}:{e}"
if _should_log_once(key):
logger.warning(
"aioice StunProtocol.%s AttributeError suppressed: %s",
send_attr_name,
e,
)
except Exception: # type: ignore
logger.exception(
"aioice StunProtocol.%s raised unexpected exception", send_attr_name
)
setattr(proto_cls, send_attr_name, _safe_send_stun) # type: ignore
logger.info("Applied safe aioice StunProtocol.%s monkeypatch", send_attr_name)
else:
logger.warning("aioice StunProtocol.send_stun not found; skipping monkeypatch")
except Exception as e:
logger.exception("Failed to apply aioice StunProtocol.send_stun monkeypatch: %s", e)