Expose per-task SLayer MCP query engine for teardown (DEV-1656)#227
Conversation
create_mcp_server now attaches its closure SlayerQueryEngine to the
returned FastMCP server as mcp._slayer_engine, the cross-repo contract
bird-interact-agents disposes at async task teardown:
engine = getattr(mcp, "_slayer_engine", None)
if engine is not None:
await engine.aclose()
This lets the cloud Ray runner (one actor reused across tasks) release
per-task asyncpg pools that would otherwise accumulate until Postgres
max_connections is exhausted.
validate_models and recommend_root_model now reuse the closure engine
instead of constructing a fresh per-call SlayerQueryEngine, so a single
engine holds every cached SQL client for the server's lifetime.
schema_drift._collect_sql_diffs now caches the SlayerSQLClient it opens
for sql-mode drift probes back into the shared sql_clients dict, so the
asyncpg pool it opens is reachable by SlayerQueryEngine.aclose() and
disposed at teardown (previously it was constructed standalone and
leaked). When no engine dict is provided the behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR updates the SQL trial-execution diffing path to cache SlayerSQLClient instances by key for lifecycle management, modifies the MCP server to store and reuse a shared SlayerQueryEngine instance across validate_models and recommend_root_model tool calls instead of creating new engines per call, and adds a comprehensive test module verifying engine reuse identity and teardown disposal behavior. ChangesShared MCP engine reuse and cached SQL client disposal
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant MCPServer
participant SlayerQueryEngine
participant SlayerSQLClient
Client->>MCPServer: call validate_models / recommend_root_model
MCPServer->>SlayerQueryEngine: reuse mcp._slayer_engine
SlayerQueryEngine->>SlayerSQLClient: get or create cached client via cache key
SlayerQueryEngine-->>MCPServer: result
MCPServer-->>Client: response
Client->>MCPServer: teardown (aclose)
MCPServer->>SlayerQueryEngine: aclose()
SlayerQueryEngine->>SlayerSQLClient: dispose cached async engines
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|



Why
Under
--query-mode slayeron a Postgres benchmark, the SLayer MCP query path opens asyncpg connection pools that are never disposed for the life of a long-running task. On the cloud runner one Ray actor process is reused across many tasks, so per-task engines/pools accumulate across tasks until Postgresmax_connectionsis exhausted. This change makes robust cloud cross-task disposal possible (contract for DEV-1654, which is coupled to this issue).Root of the un-disposability: the MCP
query/query_nestedtools run the asyncengine.execute(...)path (which, unlikeexecute_sync, does not auto-dispose), and the closure engine that owns the pools was not reachable from the returned server.Changes
slayer/mcp/server.py— attach the closure engine to the returned FastMCP server asmcp._slayer_engine(the cross-repo teardown contract). Callers dispose via:aclose()is idempotent and leaves the engine reusable (a laterexecutelazily recreates the async engine; a:memory:SQLite StaticPool survives).slayer/mcp/server.py—validate_modelsandrecommend_root_modelreuse the closure engine instead of each constructing a fresh per-callSlayerQueryEngine. One engine now holds every cached SQL client for the server's lifetime, shared acrossquery/query_nested/validate_models/recommend_root_model(all read-only introspection paths).slayer/engine/schema_drift.py—_collect_sql_diffscaches theSlayerSQLClientit opens forsql-mode drift probes back into the sharedsql_clientsdict, so the asyncpg pool it opens is reachable bySlayerQueryEngine.aclose()and disposed at teardown. Previously that client was constructed standalone and leaked even after the closure-engine reuse. When no engine dict is provided (direct/test callers), behaviour is unchanged.Tests
New
tests/test_mcp_engine_teardown.py(10 tests): the_slayer_engineattribute is the same enginequery/query_nesteduse;validate_models/recommend_root_modelconstruct no fresh engine;aclose()disposes cached clients' async engines while leaving the sync StaticPool intact (guarded);aclose()is no-op-safe and leaves the engine reusable; thesql-mode drift client is cached on the shared engine and that specific client is disposed byaclose();query-then-validate_modelsreuses the same client object.Full non-integration suite: 6611 passed, 46 skipped, 5 xfailed. Lint clean.
🤖 Generated with Claude Code
Summary by CodeRabbit