Skip to content

fix: run tool handlers off the event loop to unblock vector tools - #54

Merged
ergut merged 2 commits into
mainfrom
fix/task-4-asyncio-to-thread
Jun 6, 2026
Merged

ergut merged 2 commits into
mainfrom
fix/task-4-asyncio-to-thread

Conversation

@ergut

@ergut ergut commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Fixes #44.

Root cause was exactly as diagnosed in the issue: every ToolHandler.run_tool() is synchronous and was called directly inside the async call_tool handler, blocking the event loop. LanceDB's internal tokio runtime then deadlocked against it, so vector tools hung forever via MCP while working fine from plain Python.

Changes:

  • server.py wraps the handler call in await asyncio.to_thread(...) so the event loop stays free. This benefits all tools, not just the vector ones.
  • VectorDB.get_stats() no longer materializes every row to count unique pages; it projects the page column to Arrow and uses count_distinct.
  • New regression tests build a real (tiny) LanceDB table with a stub embedder and prove the vector handlers complete when dispatched from a running event loop.
  • Debug level timing logs around every LanceDB to_list() call and handler stage, so if the hang ever reappears the last line in mcp_logseq.log identifies the stalled stage.

Note: the deeper Windows hang reported in the issue comments could not be reproduced on macOS. The to_thread fix removes the runtime contention my analysis points to, but Windows verification is pending; the new debug logs are there to pinpoint it quickly if it persists.

ergut added 2 commits June 6, 2026 13:55
…44)

All ToolHandler.run_tool() implementations are synchronous. Before this fix,
server.py called run_tool() directly in the async event loop, blocking it for
the duration of every tool call — including LanceDB queries that carry their
own heavy I/O. Fix: wrap the call with `await asyncio.to_thread(...)` so the
event loop remains free.

Also tighten VectorDB.get_stats(): replace the `.search().to_list()` full-scan
(which builds a Python dict per row) with `.search().to_arrow()` + PyArrow
compute, reducing per-row overhead when the table is large.

Adds regression tests in tests/unit/vector/test_asyncio_regression.py that use
a real (tiny) LanceDB table and a stub embedder to prove VectorSearchToolHandler
and VectorDBStatusToolHandler complete when dispatched via asyncio.to_thread().

Note: the Windows-specific to_list() hang reported in issue #44 could not be
reproduced or verified on macOS.
Adds logger.debug instrumentation so a future Windows hang report can
identify the stalled stage from ~/.cache/mcp-logseq/mcp_logseq.log:

vector/db.py
  - search(): logs mode/top_k/filters before dispatch
  - _vector_search/_keyword_search/_hybrid_search: log "to_list start"
    and "to_list done in Xms, Y rows" around each q.to_list() call
  - get_stats(): logs count_rows timing and to_arrow+count_distinct timing

vector/index.py (VectorSearchToolHandler)
  - "embedding start (embedder=...)" before create_embedder+embed
  - "embedding done in Xms" after embed
  - "opening table at ..." before VectorDB.open_readonly
  - "table opened in Xms" after open
  - "query start (mode=...)" before db.search
  - "query done in Xms, N results" after db.search

vector/index.py (VectorDBStatusToolHandler)
  - "opening table at ..." / "table opened in Xms"
  - "get_stats done in Xms" around db.get_stats()

No behavior change; no config switch required.
@ergut
ergut merged commit 29da789 into main Jun 6, 2026
@ergut
ergut deleted the fix/task-4-asyncio-to-thread branch June 6, 2026 22:14
@ergut ergut mentioned this pull request Jun 6, 2026
ergut added a commit that referenced this pull request Jun 6, 2026
Version bump for the 1.6.3 release: #52, #53, #54, #50.
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.

vector_db_status hangs indefinitely when called via MCP (asyncio + LanceDB deadlock)

1 participant