mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-15 04:12:25 +00:00
fix(mcp): report configured timeout in MCP call errors
Track elapsed wall time in _run_on_mcp_loop, cancel the in-flight future when a timeout expires, and raise a descriptive TimeoutError that includes the elapsed and configured timeout. Add regression coverage for the new timeout diagnostics.
This commit is contained in:
parent
25187ca05c
commit
80548f9a4f
2 changed files with 45 additions and 2 deletions
|
|
@ -547,6 +547,43 @@ class TestRunOnMCPLoopInterrupts:
|
||||||
mcp_mod._mcp_loop = old_loop
|
mcp_mod._mcp_loop = old_loop
|
||||||
mcp_mod._mcp_thread = old_thread
|
mcp_mod._mcp_thread = old_thread
|
||||||
|
|
||||||
|
def test_timeout_reports_elapsed_and_configured_timeout(self):
|
||||||
|
import tools.mcp_tool as mcp_mod
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
thread = threading.Thread(target=loop.run_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
cancelled = threading.Event()
|
||||||
|
|
||||||
|
async def _slow_call():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
return "done"
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
cancelled.set()
|
||||||
|
raise
|
||||||
|
|
||||||
|
old_loop = mcp_mod._mcp_loop
|
||||||
|
old_thread = mcp_mod._mcp_thread
|
||||||
|
mcp_mod._mcp_loop = loop
|
||||||
|
mcp_mod._mcp_thread = thread
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pytest.raises(TimeoutError, match=r"MCP call timed out after .*configured timeout: 0.2s"):
|
||||||
|
mcp_mod._run_on_mcp_loop(_slow_call(), timeout=0.2)
|
||||||
|
|
||||||
|
deadline = time.time() + 2
|
||||||
|
while time.time() < deadline and not cancelled.is_set():
|
||||||
|
time.sleep(0.05)
|
||||||
|
assert cancelled.is_set()
|
||||||
|
finally:
|
||||||
|
loop.call_soon_threadsafe(loop.stop)
|
||||||
|
thread.join(timeout=2)
|
||||||
|
loop.close()
|
||||||
|
mcp_mod._mcp_loop = old_loop
|
||||||
|
mcp_mod._mcp_thread = old_thread
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tool registration (discovery + register)
|
# Tool registration (discovery + register)
|
||||||
|
|
|
||||||
|
|
@ -1942,7 +1942,8 @@ def _run_on_mcp_loop(coro, timeout: float = 30):
|
||||||
if loop is None or not loop.is_running():
|
if loop is None or not loop.is_running():
|
||||||
raise RuntimeError("MCP event loop is not running")
|
raise RuntimeError("MCP event loop is not running")
|
||||||
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
deadline = None if timeout is None else time.monotonic() + timeout
|
start_time = time.monotonic()
|
||||||
|
deadline = None if timeout is None else start_time + timeout
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if is_interrupted():
|
if is_interrupted():
|
||||||
|
|
@ -1953,7 +1954,12 @@ def _run_on_mcp_loop(coro, timeout: float = 30):
|
||||||
if deadline is not None:
|
if deadline is not None:
|
||||||
remaining = deadline - time.monotonic()
|
remaining = deadline - time.monotonic()
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
return future.result(timeout=0)
|
future.cancel()
|
||||||
|
elapsed = time.monotonic() - start_time
|
||||||
|
raise TimeoutError(
|
||||||
|
f"MCP call timed out after {elapsed:.1f}s "
|
||||||
|
f"(configured timeout: {float(timeout):.1f}s)"
|
||||||
|
)
|
||||||
wait_timeout = min(wait_timeout, remaining)
|
wait_timeout = min(wait_timeout, remaining)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue