hermes - 💡(How to fix) Fix bug(web_tools): TOCTOU race in _get_parallel_client / _get_async_parallel_client / _get_exa_client singleton init

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Three module-level lazy-init singletons in tools/web_tools.py share the same TOCTOU (time-of-check/time-of-use) race pattern:

SingletonGuardFunction
_parallel_client (line 360)if _parallel_client is None:_get_parallel_client()
_async_parallel_client (line 361)if _async_parallel_client is None:_get_async_parallel_client()
_exa_client (line 1007)if _exa_client is None:_get_exa_client()

None of them hold a lock. Under concurrent tool calls, two threads can both see None, both construct a client object, and the loser's client (with its underlying connection pool / API session) is orphaned — never closed, leaking resources.

Root Cause

Three module-level lazy-init singletons in tools/web_tools.py share the same TOCTOU (time-of-check/time-of-use) race pattern:

SingletonGuardFunction
_parallel_client (line 360)if _parallel_client is None:_get_parallel_client()
_async_parallel_client (line 361)if _async_parallel_client is None:_get_async_parallel_client()
_exa_client (line 1007)if _exa_client is None:_get_exa_client()

None of them hold a lock. Under concurrent tool calls, two threads can both see None, both construct a client object, and the loser's client (with its underlying connection pool / API session) is orphaned — never closed, leaking resources.

Fix Action

Fix

Add import threading and a per-singleton threading.Lock(). Apply double-checked locking to each of the three functions (same pattern as #24731 for openrouter_client).

RAW_BUFFERClick to expand / collapse

Summary

Three module-level lazy-init singletons in tools/web_tools.py share the same TOCTOU (time-of-check/time-of-use) race pattern:

SingletonGuardFunction
_parallel_client (line 360)if _parallel_client is None:_get_parallel_client()
_async_parallel_client (line 361)if _async_parallel_client is None:_get_async_parallel_client()
_exa_client (line 1007)if _exa_client is None:_get_exa_client()

None of them hold a lock. Under concurrent tool calls, two threads can both see None, both construct a client object, and the loser's client (with its underlying connection pool / API session) is orphaned — never closed, leaking resources.

Fix

Add import threading and a per-singleton threading.Lock(). Apply double-checked locking to each of the three functions (same pattern as #24731 for openrouter_client).

Impact

  • Duplicate Parallel / AsyncParallel / Exa client instances → orphaned connection pools
  • Wasted TCP connections and potential rate-limit double-counting
  • Observed under any concurrent tool dispatch (MoA, parallel tool calls)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix bug(web_tools): TOCTOU race in _get_parallel_client / _get_async_parallel_client / _get_exa_client singleton init