Skip to content

Fix #1273: bug: Thread leak in searcher._retrieve_paths causes container thread exhaustion#2134

Open
Memtensor-AI wants to merge 3 commits into
MemTensor:dev-v2.0.25from
Memtensor-AI:feature/autodev-1273-20260721042041846
Open

Fix #1273: bug: Thread leak in searcher._retrieve_paths causes container thread exhaustion#2134
Memtensor-AI wants to merge 3 commits into
MemTensor:dev-v2.0.25from
Memtensor-AI:feature/autodev-1273-20260721042041846

Conversation

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Description

Fixes #1273 (thread leak in Searcher retrieval paths). Searcher previously opened a fresh ContextThreadPoolExecutor per /search request in four methods (_retrieve_paths, _retrieve_from_long_term_and_user, _retrieve_from_tool_memory, _deduplicate_rawfile_results); any hung sub-task blocked shutdown(wait=True) and leaked threads, growing container thread count without bound (reported 8,744 threads leading to empty /search results, /chat 503s, and OpenBLAS pthread_create failures).

The fix pre-allocates four class-level shared ContextThreadPoolExecutor instances in Searcher.__init__ (mirroring the original per-site max_workers 5/3/2/10, with distinct thread_name_prefix for observability), rewrites the four call sites to submit to the shared executor without a with block, and adds a SEARCH_FUTURE_RESULT_TIMEOUT = 30.0 module constant applied to every future.result() / as_completed() with warning-level exception handling. Total worker thread ceiling per Searcher drops from unbounded to 20 regardless of QPS. Four separate pools (not one shared) prevent the deadlock that would arise from nested submits between _retrieve_paths and its sub-paths.

Tests: 4 new regression tests lock in shared executor identity across calls, absence of naked .result(), and survival under a 5x-timeout slow sub-task. All 4 pre-existing behavioural tests still pass; wider tests/memories/textual/ suite is 62/62 green. Ruff check + format both clean. Two pre-existing tests/memories/activation/test_kv.py failures (transformers DynamicCache API drift) are unrelated and reproduced on the stashed tree.

Files touched: src/memos/memories/textual/tree_text_memory/retrieve/searcher.py, tests/memories/textual/test_tree_searcher.py. No API schema, config, or dependency changes.

Related Issue (Required): Fixes #1273

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

Not run; documentation-only change.

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • I have created related documentation issue/PR in MemOS-Docs (if applicable)
  • I have linked the issue to this PR (if applicable)
  • I have mentioned the person who will review this PR

@bittergreen please review this PR.

Reviewer Checklist

…aths

Refs: MemTensor#1273

`Searcher` used to open a fresh `ContextThreadPoolExecutor` on every
`/search` request in four methods (`_retrieve_paths`,
`_retrieve_from_long_term_and_user`, `_retrieve_from_tool_memory`,
`_deduplicate_rawfile_results`). If any submitted task hung (slow
Neo4j, unresponsive embedding API), the `with` block blocked in
`shutdown(wait=True)` and the worker threads were never freed. Over
hours/days, container thread count grew without bound (users reported
8,744 threads), at which point `/search` silently returned empty,
`/chat` returned 503, and OpenBLAS could no longer `pthread_create`.

Fix:

- Pre-allocate four class-level shared `ContextThreadPoolExecutor`
  instances in `Searcher.__init__`, mirroring the original
  `max_workers` per site (5 / 3 / 2 / 10) with distinct
  `thread_name_prefix` for observability. Total ceiling per Searcher
  drops from unbounded to 20 worker threads regardless of QPS.
- Rewrite the four call sites to submit to the shared executor without
  a `with` block, so the pool outlives the request.
- Add `SEARCH_FUTURE_RESULT_TIMEOUT = 30.0` module constant and pass
  it to every `future.result()` and `as_completed()` in the four
  methods, catching timeouts with a warning so a hung sub-task cannot
  block a request forever.

Kept four separate pools instead of one shared pool: `_retrieve_paths`
workers call into `_retrieve_from_long_term_and_user` /
`_retrieve_from_tool_memory` (nested submits). A single pool with
`max_workers < fan-out` would deadlock; disjoint pools mirror the
original semantics 1:1 and stay deadlock-free.

Tests: 4 new regression tests locking in the fix (shared executor
identity across calls, no naked `.result()`, survival under 5x-timeout
slow sub-task), plus the 4 existing behavioural tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Memtensor-AI Memtensor-AI added ai:generated Generated or modified by AI | 由 AI 生成或修改 area:memory 记忆存储、检索、更新、召回逻辑 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 21, 2026
@Memtensor-AI
Memtensor-AI requested a review from bittergreen July 21, 2026 04:48
@Memtensor-AI

Memtensor-AI commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Open Code Review

Target: PR #2134
Task: b1848bec14f3201e
Base: dev-v2.0.25
Head: feature/autodev-1273-20260721042041846
Head SHA: 4a885bf53af01cf8f619adcbc5fb890ac9bacd34

OpenCodeReview: No comments generated. Looks good to me.

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 6 issue(s). I have resumed the development Agent to fix them.

  • Task: b1848bec14f3201e
  • Fix attempt: 1/2
  • Finding delta: 0 repeated / 6 new / 0 likely resolved

The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

… tests

- drop redundant inner timeout on future.result() inside as_completed loop
- add Searcher.close() + context-manager protocol to shut down the five
  shared executors so worker threads do not outlive the instance
- tests: guard private _max_workers access, use valid mode='fine' in pool
  reuse test, hoist inspect import, replace string matching with AST-based
  detection of unbounded future.result() calls

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 2 issue(s). I have resumed the development Agent to fix them.

  • Task: b1848bec14f3201e
  • Fix attempt: 2/2
  • Finding delta: 1 repeated / 1 new / 5 likely resolved

Repeated findings are prioritized because the previous repair did not converge.
The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

…1273 tests

Replace all _max_workers/_shutdown accesses with public-API behavioral
checks per OCR round 2:
- verify pool worker bounds by submitting N+1 blocking tasks and
  asserting exactly N run concurrently
- verify live/shut-down state via submit() succeeding vs raising
  RuntimeError (documented Executor.shutdown() behavior)

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

ai:generated Generated or modified by AI | 由 AI 生成或修改 area:memory 记忆存储、检索、更新、召回逻辑 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants