Skip to content

fix(mcp): heartbeat deadlock, chart inline default, lock-free status fast path (#118)#126

Merged
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:ssteiner/fix-heartbeat-deadlock
Jun 9, 2026
Merged

fix(mcp): heartbeat deadlock, chart inline default, lock-free status fast path (#118)#126
StefanSteiner merged 4 commits into
tableau:mainfrom
StefanSteiner:ssteiner/fix-heartbeat-deadlock

Conversation

@StefanSteiner

@StefanSteiner StefanSteiner commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Four fixes on this branch:

  1. Fix heartbeat deadlock. maybe_send_heartbeat() re-locked self.engine inside with_engine(), which already holds that mutex — under contention this would deadlock and freeze every tool call. The fix passes the daemon health port directly from the caller's already-held engine reference. Fixes MCP: Windows MCP v0.5.2 is broken. Trying to run queries will hang the MCP. This will be fixed soon. #125.

  2. Default chart inline to true. MCP clients (Cursor, Claude Desktop) couldn't render charts without a separate file-read round-trip because the chart tool defaulted to inline=false. Flipping the default means charts return base64 image bytes inline for immediate display. Callers can still set inline=false for disk-only output.

  3. Remove unused Duration import in connect_named_pipe (Windows-only warning from the keepalive change).

  4. Lock-free status fast path (partially addresses MCP: single engine mutex + no op timeout lets one stalled hyperd call hang all tool calls #118). The status tool previously went through with_engine, blocking behind the global engine mutex. If another tool call was stalled (half-open connection, long query), status would hang indefinitely. Now status uses try_lock():

    • If the lock is available: full response (table counts, disk, endpoints).
    • If the lock is held OR engine not yet initialized: returns a degraded-but-instant response (daemon health via discover() — max ~300ms, no port scan; persistent path; watchers; attachments; version) with engine_busy: true.

    This decouples diagnostics from the data-plane lock so status never hangs. Other tool calls (query, execute, etc.) still serialize behind the engine mutex — the general serialization issue (connection pool / per-op timeout) remains tracked in MCP: single engine mutex + no op timeout lets one stalled hyperd call hang all tool calls #118.

Test plan

  • cargo test -p hyperdb-mcp — all tests pass
  • cargo clippy --all-targets -- -D warnings clean
  • Manual: start the MCP, call status while a long query is running — confirm instant response with engine_busy: true
  • Manual: call status normally — confirm full response with engine_busy: false
  • Manual: call chart(...) without inline — confirm base64 image returned inline
  • Manual: run several tool calls in daemon mode to verify heartbeat doesn't hang

StefanSteiner and others added 3 commits June 9, 2026 14:45
Duration is already imported at file scope (line 39); the local
re-import inside the #[cfg(windows)] function body triggered
-W redundant-imports.
maybe_send_heartbeat() was re-locking self.engine inside with_engine(), which already holds that mutex. Pass the daemon health port from the caller's engine reference instead of re-acquiring the lock.

Co-authored-by: Cursor <cursoragent@cursor.com>
MCP clients (Cursor, Claude Desktop) could not render charts without a separate file-read round-trip because the chart tool defaulted to inline=false (disk-only). Flip the default so charts return base64 image bytes inline, eliminating the extra step.

Co-authored-by: Cursor <cursoragent@cursor.com>
@StefanSteiner StefanSteiner force-pushed the ssteiner/fix-heartbeat-deadlock branch from 34233eb to 8792ef3 Compare June 9, 2026 21:46
…au#118)

The `status` tool previously went through `with_engine`, which takes
the global engine mutex and blocks until any in-progress tool call
releases it. If a data-plane operation was stalled (e.g. a half-open
connection blocking a read, or a legitimately long analytics query),
`status` would hang behind it indefinitely with no timeout.

Fix: `status` uses `try_lock()` instead of blocking `lock()`. If the
engine mutex is available, it runs the full `Engine::status()` (table
counts, disk usage, endpoints). If the mutex is held OR the engine is
not yet initialized, it returns a **degraded-but-instant** response
built from metadata available without the engine: daemon health (via
`discover()` — a fast file-read + single PING to the known port, max
~300ms; deliberately NOT `find_running_daemon()` which adds a 16-port
scan that could block 5s), the persistent path, watchers, attachments,
and read-only state. The response includes `engine_busy: true`.

Scope: this decouples `status` from the engine mutex so diagnostics
never hang. Other data-plane tool calls (`query`, `execute`, etc.)
still serialize behind the engine lock — the general serialization
issue (connection pool or per-op timeout) is tracked separately.

Partially addresses tableau#118.
@tableau tableau deleted a comment from salesforce-cla Bot Jun 9, 2026
@StefanSteiner StefanSteiner changed the title fix(mcp): heartbeat deadlock + chart inline default fix(mcp): heartbeat deadlock, chart inline default, lock-free status fast path (#118) Jun 9, 2026
@StefanSteiner StefanSteiner merged commit 8115109 into tableau:main Jun 9, 2026
12 checks passed
@StefanSteiner StefanSteiner mentioned this pull request Jun 9, 2026
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.

MCP: Windows MCP v0.5.2 is broken. Trying to run queries will hang the MCP. This will be fixed soon.

1 participant