Files
hermes-agent/agent/async_utils.py

51 lines
2.0 KiB
Python

"""Async/sync bridging helpers.
``asyncio.run_coroutine_threadsafe`` can raise ``RuntimeError`` (loop closed during a
shutdown race); the coroutine is then never awaited or closed, which triggers a
"coroutine was never awaited" RuntimeWarning and leaks its frame. The helpers here
close the coroutine on scheduling failure. ``future.result()`` failures are deliberately
NOT handled: once the loop accepts the coroutine its lifecycle belongs to the loop.
"""
from __future__ import annotations
import asyncio
import logging
from concurrent.futures import Future
from typing import Any, Coroutine, Optional
_DEFAULT_LOGGER = logging.getLogger(__name__)
def safe_schedule_threadsafe(
coro: Coroutine[Any, Any, Any], loop: Optional[asyncio.AbstractEventLoop], *,
logger: Optional[logging.Logger] = None,
log_message: str = "Failed to schedule coroutine on loop", log_level: int = logging.DEBUG,
) -> Optional[Future]:
"""Schedule ``coro`` on ``loop`` from a sync context, leak-safe.
Returns the Future on success, or ``None`` if the loop is missing or scheduling
raised; in every failure path the coroutine is closed. Callers keep full control
over the returned future (``.result(timeout=...)``, callbacks, fire-and-forget).
"""
log = logger if logger is not None else _DEFAULT_LOGGER
try:
if loop is None:
raise RuntimeError("loop is None")
return asyncio.run_coroutine_threadsafe(coro, loop)
except Exception as exc:
if asyncio.iscoroutine(coro):
coro.close()
log.log(log_level, "%s: %s", log_message, exc)
return None
def consume_detached_task_result(task: "asyncio.Future[Any]") -> None:
"""``add_done_callback`` for cancelled-and-detached tasks: observe the exception so the
loop does not log "exception was never retrieved"; cancellation and terminal errors
are swallowed because the task's owner already gave up on it."""
try:
task.exception()
except (asyncio.CancelledError, Exception):
pass