Skip to content

refactor(ddgs): replace global ThreadPoolExecutor with per-call bounded executor#445

Merged
deedy5 merged 1 commit into
mainfrom
dev
Apr 3, 2026
Merged

refactor(ddgs): replace global ThreadPoolExecutor with per-call bounded executor#445
deedy5 merged 1 commit into
mainfrom
dev

Conversation

@deedy5

@deedy5 deedy5 commented Apr 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Remove shared _executor ClassVar and get_executor() method
  • Use per-call ThreadPoolExecutor with context manager for automatic cleanup
  • Add DDGS.threads cap to limit max_workers per search
  • Fix bug: log f_engine.name instead of engine.name in exception handler
  • Prevents thread pool starvation under concurrent load

…ed executor

- Remove shared _executor ClassVar and get_executor() method
- Use per-call ThreadPoolExecutor with context manager for automatic cleanup
- Add DDGS.threads cap to limit max_workers per search
- Fix bug: log f_engine.name instead of engine.name in exception handler
- Prevents thread pool starvation under concurrent load
@deedy5 deedy5 merged commit 3d9b5f0 into main Apr 3, 2026
0 of 12 checks passed
@deedy5

deedy5 commented Apr 3, 2026

Copy link
Copy Markdown
Owner Author

Fix microsoft/amplifier#219

@deedy5 deedy5 deleted the dev branch April 3, 2026 08:44
shenjianan97 added a commit to shenjianan97/persistent-agent-runtime that referenced this pull request Apr 16, 2026
The previous threading.Lock serialized every web_search call to work
around a thread pool starvation bug in ddgs < 9.12.1, where a process-
wide ClassVar ThreadPoolExecutor was shared across all DDGS instances.
Under concurrent callers the shared pool saturated, and combined with
DDG's per-IP rate limiting this presented as "blocked forever" (see
commit 310be71 which introduced the lock).

ddgs 9.12.1 (upstream PR deedy5/ddgs#445) replaced the shared pool with
a per-call, context-managed ThreadPoolExecutor. Each search now owns an
isolated, bounded executor that shuts down on exit, so inter-call pool
contention is gone.

- Pin ddgs>=9.12.1 so we can't slide back into the broken range
- Drop threading.Lock and thread import
- Introduce asyncio.Semaphore(max_concurrent=3) on the async side: lets
  up to 3 searches run in parallel via asyncio.to_thread while still
  capping fan-out to stay under DDG's per-IP rate limit
- max_concurrent is a constructor knob so the tuning can move without
  a code change when we learn more from production load

Adds 4 unit tests verifying the semaphore actually caps in-flight work,
parallel wall-clock beats serial, and the threading.Lock is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant