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)