Skip to content

⚡ perf(mcp): 隔离同步数据库调用与事件循环 - #156

Merged
ZhaoXingPeng merged 1 commit into
mainfrom
perf/155-async-db-boundary
Sep 9, 2026
Merged

ZhaoXingPeng merged 1 commit into
mainfrom
perf/155-async-db-boundary

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Sep 8, 2026

Copy link
Copy Markdown
Owner

关联 Issue

Closes #155

本 PR 将同步数据库调用移出 MCP 事件循环,并为共享连接建立明确的并发边界。

背景(Situation)

MCP handler 使用 async def,但查询、发现、元数据、代码生成分析和 ER 路径直接调用同步数据库驱动。网络等待或大型 SQLite 查询会占住事件循环;SQLite 单连接跨 worker 使用也需要线程配置和串行策略。

任务(Task)

在不替换同步驱动、不改变 SQL、事务、权限、同步 API 或 MCP 响应合同的前提下,将阻塞数据库阶段移入 worker;保证同一连接串行、不同连接可并行等待,并覆盖关闭竞争和 SQLite worker。

行动(Action)

  • ConnectionManager 增加注册表 RLock 和按 connection_id 的可重入锁,锁覆盖健康探测、cursor 生命周期、execute 和 close。
  • SQLite 使用 check_same_thread=False,仅允许在连接锁保护下交给 worker。
  • mcp_tools 增加统一数据库 worker helper,迁移查询、元数据、连接探测和代码生成分析。
  • 原子 codegen 与 ER 可视化入口也迁移阻塞数据库阶段;保留无参数 execute_query 的两参数替身合同。
  • 没有做:未引入原生异步驱动、连接池、TTL 或生产吞吐承诺。

验证(Verification)

环境:Windows;Python 3.10.1;pytest 9.0.3;pytest-asyncio 1.3.0;SQLite 内存/文件 fixture。

  • 单元测试:685 passed in 5.24s。
  • SQLite MCP 查询与文件 codegen 合同:3 passed in 3.45s。
  • 异步边界测试:5 passed。
  • Ruff check:All checks passed;修改文件格式检查通过;git diff --check 无输出。

实验与证据(Evidence)

受控 driver 在 execute 内阻塞 60ms,同时 heartbeat 每 2ms 运行 40ms,等待期间获得至少 2 次调度。同连接 fake cursor 最大活动数为 1;两个不同 connection_id 可同时进入 worker;close 在 query 完成前等待并在之后清理连接、配置和锁。SQLite worker 使用 SELECT 1 AS value,无 thread-affinity 错误。以上为受控 stub 与 SQLite 证据,不代表真实网络数据库吞吐。

兼容性、风险与回滚

保留同步 ConnectionManager 方法签名、异常类型、SQL、事务和 Raw Response。线程调度增加少量开销;同一连接仍串行;取消 async handler 不会强杀底层 worker。真实 MySQL/PostgreSQL 驱动线程模型需在容器中验证。恢复提交 48da618 可移除本主题,不涉及用户数据迁移;后续建立固定数据库、查询集和硬件条件下的性能基线。

@ZhaoXingPeng

Copy link
Copy Markdown
Owner Author

阶段回帖 1/2:实现完成

来源与范围

实现证据

  • connection_manager.py

    • 连接注册表由短时 RLock 保护。
    • 每个 connection_id 分配一个 threading.RLockget_cursor 持锁覆盖健康探测、cursor 创建、调用方 SQL body 和 cursor close。
    • execute_query、健康探测和 close_connection 因而在同一连接上串行;不同连接使用不同锁,可同时等待。
    • 关闭完成后从 connections、connection_configs、_connection_locks 同步移除;SQL body 异常只由既有 DatabaseQueryError 路径处理,不误删健康连接。
    • SQLite 创建时显式使用 check_same_thread=False,跨 worker 使用仍受上述锁约束。
  • mcp_tools.py

    • _run_db_call 统一使用 asyncio.to_thread
    • _run_async_db_call 将现有 async analyzer(内部同步 introspection)放入 worker event loop。
    • 连接初始探测、数据库/表查询、表存在性、只读 SQL、columns/keys/indexes、describe、codegen analyze/generate 全部经过边界。
    • params is None 时只传两个位置参数,兼容既有 monkeypatch/test double;带参数的 SQL 保持三参数调用。
  • atomic_codegen_tools.py

    • codegen_build_context 的表名收集与分析阶段也迁移到 worker。
    • 表名收集优先使用受锁的 get_cursor,保留旧 test double 的兼容 fallback。
  • visualization_tools.py

    • ER 图每张表的列/FK introspection 在 worker 中执行,渲染和 MCP App meta 仍在事件循环做纯内存处理。

设计边界

  • 保留同步 ConnectionManager,没有替换驱动、连接池或事务模型。
  • 不宣称吞吐收益;本次目标是事件循环响应性和单连接线程安全。
  • 取消 handler 不强杀底层线程,worker 返回后由 context manager 释放 cursor 和锁。

@ZhaoXingPeng

Copy link
Copy Markdown
Owner Author

阶段回帖 2/2:验证完成

环境与输入

  • 环境:Windows,Python 3.10.1(仓库声明 >=3.11),pytest 9.0.3,pytest-asyncio 1.3.0,SQLite 内存/文件 fixture。
  • 代码输入:提交 48da618,未跟踪的用户文件 uv.lock 未纳入提交。
  • 阻塞模型:受控 driver stub 在 execute 内 sleep 60 ms;并发测试使用 threading.Event 控制进入/释放。

命令与原始结果

$env:PYTHONPATH='src'; python -m pytest tests/unit/ -q
685 passed in 5.24s

$env:PYTHONPATH='src'; python -m pytest tests/integration/test_sqlite_mcp_query_contract.py tests/integration/test_sqlite_file_codegen_contract.py -q
3 passed in 3.45s

$env:PYTHONPATH='src'; python -m pytest tests/unit/test_async_db_boundary.py -q
5 passed

ruff check src/dbjavagenix/database/connection_manager.py src/dbjavagenix/database/mcp_tools.py src/dbjavagenix/database/atomic_codegen_tools.py src/dbjavagenix/database/visualization_tools.py tests/unit/test_async_db_boundary.py
All checks passed!

ruff format --check src/dbjavagenix/database/atomic_codegen_tools.py src/dbjavagenix/database/visualization_tools.py tests/unit/test_async_db_boundary.py
3 files already formatted

git diff --check
(no output)

验收映射

  • 事件循环哨兵:阻塞 stub 运行期间 heartbeat 仍获得调度(断言 ticks >= 2)。
  • 同连接并发:最大活动 cursor 数断言为 1,第二次调用等待第一调用释放。
  • 不同连接并发:两个连接均能在释放前进入 worker,证明没有全局锁。
  • close/execute 竞争:close 在 SQL body 完成前等待;完成后 cursor 已关闭,connection/config/per-connection lock 均清理。
  • SQLite worker:SELECT 1 AS value 在 worker thread 返回 [{"value": 1}],无 thread-affinity 错误。
  • 既有合同:完整 unit、PostgreSQL MCP mock、MCP query safety、SQLite MCP/codegen contract 全部通过;SQL 语句失败仍保留连接并返回既有异常路径。

边界与未执行项

  • 本地未运行真实 MySQL/PostgreSQL 容器,因此不报告真实网络驱动吞吐或线程模型通过。
  • 未查询 CI;按项目约定,仅在准备 squash merge 前对最终 HEAD 检查一次 required checks。
  • 全仓库 ruff format --check src 会触及历史 legacy 文件,本 PR 只验证本次修改文件,避免无关格式化 churn。

@ZhaoXingPeng
ZhaoXingPeng force-pushed the perf/155-async-db-boundary branch from 48da618 to 58c4648 Compare September 9, 2026 01:51
@ZhaoXingPeng
ZhaoXingPeng merged commit 63ab339 into main Sep 9, 2026
12 checks passed
@ZhaoXingPeng
ZhaoXingPeng deleted the perf/155-async-db-boundary branch September 9, 2026 01:57
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.

⚡ perf(mcp): 隔离同步数据库调用与事件循环

1 participant