fix(auxiliary_client): skip async close() during cache eviction - #45337
liuhao1024 wants to merge 1 commit into
Conversation
_store_cached_client() and the manual cache-eviction path called close() synchronously on evicted clients. For AsyncOpenAI clients, close() is a coroutine — calling it without await creates an unawaited coroutine RuntimeWarning. Fix: add `inspect.iscoroutinefunction` guard (matching the existing pattern in shutdown_cached_clients()) so async clients are only neutered via _force_close_async_httpx, not via synchronous close(). Fixes NousResearch#45328
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Clean, well-scoped fix/feature with comprehensive tests. No issues found.
- Logic is correct and focused
- Tests cover the new behavior
- No security concerns
- Good error handling
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused fix. The premise remains live on current main: agent/auxiliary_client.py:3221-3223 and agent/auxiliary_client.py:5563-5565 still synchronously invoke cached clients' close() methods, while shutdown_cached_clients() already skips declared coroutine close methods at agent/auxiliary_client.py:5688-5690.
Problems
- The new
if close_fn and ...condition drops the existingcallable(close_fn)guard. Retain it so a truthy non-callablecloseattribute is not invoked. - The guard only covers methods declared with
async def; a callable that returns a coroutine would still leak that coroutine from this synchronous path. Issue #45328 identifies that broader cleanup concern. The added tests cover_store_cached_client()but not the separate live_evict_cached_clients()path atagent/auxiliary_client.py:3208-3226.
Suggested changes
- Centralize the cached-client close policy and use it from both eviction paths.
- Add a regression test for provider-wide eviction, alongside the replacement-path test.
Automated hermes-sweeper review.
| _force_close_async_httpx(client) | ||
| try: | ||
| import inspect | ||
| close_fn = getattr(client, "close", None) |
There was a problem hiding this comment.
Please retain callable(close_fn) here. close_fn being truthy does not establish that it can be called; the previous guard preserved that distinction before applying the async-close exclusion.
| _force_close_async_httpx(old_entry[0]) | ||
| try: | ||
| import inspect | ||
| close_fn = getattr(old_entry[0], "close", None) |
There was a problem hiding this comment.
This declaration-level check still invokes a regular callable that returns a coroutine, then discards that coroutine. Consider routing both eviction sites through one helper that captures the result and closes a returned coroutine, as described in #45328.
What does this PR do?
Fixes unawaited coroutine warnings during auxiliary-client cache eviction. When
_store_cached_client()evicts an old client whoseclose()method is a coroutine (e.g., AsyncOpenAI), calling it synchronously creates aRuntimeWarning: coroutine 'AsyncAPIClient.close' was never awaited. The fix adds aninspect.iscoroutinefunctionguard matching the existing pattern inshutdown_cached_clients().Related Issue
Fixes #45328
Type of Change
Changes Made
agent/auxiliary_client.py: Addedinspect.iscoroutinefunctionguard in_store_cached_client()and the manual cache-eviction path (lines ~4372 and ~2612) to skip synchronousclose()on async clients —_force_close_async_httpx()already handles transport teardowntests/agent/test_auxiliary_cache_async_close.py: 2 tests verifying async close is skipped (no unawaited coroutine warning) and sync close is still calledHow to Test
pytest tests/agent/test_auxiliary_cache_async_close.py -v— both tests should pass_client_cachewith anAsyncCloseClient, replaces it via_store_cached_client(), and asserts zeroRuntimeWarningabout unawaited coroutinesclose()methods are still called during evictionChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/ACode Intelligence
agent/auxiliary_client.py::_store_cached_client(callers: cache refresh paths, 2 call sites fixed)shutdown_cached_clients()guard at line 4497