"""Timeout / fallback wrappers for optional external dependencies.
:func:`guard_async_dependency` runs a coroutine under an
:func:`asyncio.wait_for` timeout and returns a safe fallback value
(logging a warning) on timeout or error, while always re-raising
:class:`asyncio.CancelledError`. Used to keep a slow or unavailable
dependency (RAG, web search, …) from stalling the message pipeline.
"""
import asyncio
import logging
from typing import Any, Coroutine
logger = logging.getLogger(__name__)
[docs]
async def guard_async_dependency(
coro: Coroutine[Any, Any, Any],
timeout_seconds: float,
fallback_value: Any,
service_name: str
) -> Any:
"""Run a coroutine under a timeout, returning a fallback instead of failing.
Awaits *coro* inside :func:`asyncio.wait_for` so a slow or unavailable
optional dependency (RAG, web search, an external API, etc.) cannot stall the
message pipeline. On timeout it logs a warning and returns *fallback_value*;
on any other exception it logs an error (with traceback) and likewise returns
the fallback. :class:`asyncio.CancelledError` is deliberately re-raised so
task cancellation is never swallowed. The only side effect is a log line via
this module's ``logger``.
Called wherever a non-critical async dependency is invoked along the hot
path; also covered directly by
``tests/core/migration/test_dependency_guards.py``.
Args:
coro: The coroutine to await under the timeout. It is consumed exactly
once whether or not it completes in time.
timeout_seconds: Maximum seconds to wait before falling back.
fallback_value: Value returned on timeout or error.
service_name: Label for the guarded dependency, used in log messages.
Returns:
Any: The coroutine's result on success, otherwise *fallback_value*.
Raises:
asyncio.CancelledError: Always re-raised; cancellation is never caught.
"""
try:
return await asyncio.wait_for(coro, timeout=timeout_seconds)
except asyncio.TimeoutError:
logger.warning(
f"dependency_timeout service={service_name} timeout_s={timeout_seconds} fallback=True"
)
return fallback_value
except asyncio.CancelledError:
# Never swallow cancellation
raise
except Exception as e:
logger.error(
f"dependency_failure service={service_name} error={str(e)} fallback=True",
exc_info=True
)
return fallback_value