Dashboard 后端稳定性:修两处连接/线程泄漏 + 首页指标加悬停说明
部署上线后,看板在真实访问下会"数据转圈加载不出、整页卡死"。定位下来是两处后端泄漏,前端 Phase 1(#31)未触及。本任务修这两处,并顺带给首页 9 张指标卡加一句话悬停说明(纯前端、已逐项核对字段含义)。
代码定位若行号漂移,以函数名/字段名/语义为准。
Part 1 · P0 卡死(后端,必须修)
泄漏 A — SSE 占满工作线程
web/app.py:1149 /api/events 是 while True: yield ...; time.sleep(2) 的同步生成器,以 text/event-stream 返回。连接存活期间一直占住一个 waitress 线程,永不释放。
- 前端
web/static/js/app.js:188 new EventSource('/api/events') 加载即开;断线自动重连。
- waitress 线程池有限(
main.py:92,已临时从 8 调到 64,仅缓解)。每个标签页/重连占 1 线程,池满后所有请求永久排队 → 卡死。
改法(推荐:短轮询,根治):
- 后端:把
/api/events 改成立即返回即关闭的普通接口 GET /api/events?since=<seq>,一次性返回 since 之后的新事件 JSON,不再 while True。
- 前端:去掉
EventSource,改 setInterval(fetchEvents, 2000),带上次 seq。
main.py 的 threads 可恢复到合理值(修好后不应再依赖 64 这个临时缓解);若保留 SSE 方案则必须保证 SSE 不占数据库连接、且标签页隐藏时 eventSource.close()、重连指数退避——但短轮询更省心,优先短轮询。
泄漏 B — 读接口不结束事务,连接卡在 idle in transaction
这是 /api/stats 实测 3/3 超时 25s 的真正原因,和泄漏 A 是同一类问题的两个面。
db/database.py:118 连接 autocommit=False,每线程一条、长期复用、从不关闭(get_conn() 线程局部)。
db/database.py:825/835 的 fetchone/fetchall 只 execute、从不 commit/rollback。纯读请求(看板绝大多数接口)跑完第一个 SELECT 后,事务一直开着不关。
- 后果:postgres 里堆出一批
idle in transaction 连接(线上实测约 12 条),占连接槽、持旧快照。线程调到 64 后最多能攒 64 条卡住的连接 → 顶满连接数/卡锁 → 落到冷线程的 /api/stats 永久排队。
改法(推荐:请求结束统一释放事务):
- 在
web/app.py 加 @app.teardown_request,请求结束时 db.rollback()(包 try/except,鲁棒)。读路径本就没东西可丢,写路径返回前已 commit(),此处 rollback 是空操作。一个钩子即可清掉所有 idle-in-transaction。
- 连接异常时也要 rollback,避免坏连接被复用(
get_conn() 复用前可加一次健康检查或异常时重建)。
Part 2 · 首页指标悬停说明(前端,小改)
首页 9 张指标卡只有名字没有解释,用户会误读(尤其"文献"显示的是已处理数不是入库总数;"研究洞见"和"深度发现"是两张不同的表)。给每张卡加 title 悬停说明,走现有 i18n(data-i18n-title,applyI18n 已支持)。
字段含义已逐项核对(db/database.py:get_stats() + app.js:165-173 绑定),按下表加 tip(新增 i18n 键,en/zh 键集合必须相等):
| 卡片 (id) |
绑定字段 |
zh tip |
en tip |
文献 statPapers |
papers_processed |
已抽取/解析完成的文献数(不含仅入库未处理的) |
Papers extracted/parsed (excludes fetched-but-unprocessed) |
基准结果 statResults |
results_total |
基准实验产出的结果记录条数 |
Benchmark result records produced by experiments |
研究领域 statTaxonomy |
taxonomy_nodes_total |
研究领域分类树的节点数 |
Nodes in the research-area taxonomy tree |
矛盾 statContradictions |
contradictions_total |
从文献中发现的互相矛盾的论断对数 |
Pairs of conflicting claims found across papers |
研究洞见 statInsights |
insights_total |
研究洞见条数(insights 表;与"深度发现"是不同的表) |
Research insights (insights table; distinct from Discoveries) |
Token statTokens |
tokens_consumed |
处理文献累计消耗的 LLM token 总量 |
Total LLM tokens consumed processing papers |
实验运行 statExperiments |
experiment_runs_total |
实验运行的总次数 |
Total experiment runs |
深度发现 statDeepDiscoveries |
deep_insights_total |
可发展成论文的深度发现条数(deep_insights 表;与"研究洞见"是不同的表) |
Discoveries that can grow into papers (deep_insights table; distinct from Research Insights) |
投稿包 statCompletePapers |
submission_bundles_total |
打包完成、可投稿的完整论文数 |
Completed paper bundles ready for submission |
实现:在 web/templates/index.html 每张 .stat-card 上加 data-i18n-title="overview.xxx.tip"(或在卡内 label 上),在 web/static/js/i18n.js 的 en/zh 各加对应 *.tip 键。
Allowed / Forbidden
- Allowed:
web/app.py(加 teardown、改 /api/events)、main.py(threads 值)、db/database.py(连接健康/rollback 相关,仅限本任务所需)、web/static/js/app.js(SSE→轮询)、web/templates/index.html + web/static/js/i18n.js(tip)、相关测试。
- Forbidden: 不改其它后端 API 的返回字段/语义;不改统计口径(各 COUNT 的 SQL 不动,只解释含义);不动
agents//orchestrator//contracts/ 的业务逻辑;不引前端框架;不删 tab/功能;不碰数据库 schema。
执行约定
- 先加失败/回归测试,再改代码。
- 每部分最小必要改动,不顺手重构。
- 不弱化现有测试与门禁。
- PR 描述列:changed files / tests added / tests run / before-after / non-goals。
Test Execution Protocol
- 基线:
pytest tests/test_web_app.py -q
- 新增本任务测试(见验收)
- 实现
- 跑本任务测试 + 再跑基线
- PR 贴实际命令与结果
验收(可机验 + 线上复核)
Part 1:
- 加压一批只读请求后,
SELECT count(*) FROM pg_stat_activity WHERE state='idle in transaction' → 归零(或个位且不随请求数增长)。
/api/stats 在有其它长连接/并发读时,p95 < 1s、稳定 200(不再 25s 超时)。
- SSE 改造后:持续打开页面/多标签不再单调吃光 waitress 线程;若保留 SSE,需证明其不占数据库连接。
python -c "import web.app" ok;pytest tests/test_web_app.py -q 全绿;Flask 起来后 /、/api/stats、/api/events(或新 ?since=)均 200。
Part 2:
5. i18n.js 新增 9 个 *.tip 键,en/zh 键集合仍完全相等(沿用现有 parity 测试)。
6. 9 张卡都有可见 title/tooltip,文案与上表一致;切换 EN/ZH 时 tip 跟随。
实现红线: idle-in-transaction 必须真归零(不是把阈值调大蒙混);/api/stats 必须真快(不是加 cache 把旧值返回了事——除非明确实现并说明 cache 策略);tip 的 en/zh 键必须对齐。
Non-goals
重部署
host 上 git fetch && git checkout <分支> → sudo systemctl restart deepgraph-web → 复核 /api/stats 秒回、多标签不卡死、指标卡有悬停说明。
Dashboard 后端稳定性:修两处连接/线程泄漏 + 首页指标加悬停说明
部署上线后,看板在真实访问下会"数据转圈加载不出、整页卡死"。定位下来是两处后端泄漏,前端 Phase 1(#31)未触及。本任务修这两处,并顺带给首页 9 张指标卡加一句话悬停说明(纯前端、已逐项核对字段含义)。
代码定位若行号漂移,以函数名/字段名/语义为准。
Part 1 · P0 卡死(后端,必须修)
泄漏 A — SSE 占满工作线程
web/app.py:1149/api/events是while True: yield ...; time.sleep(2)的同步生成器,以text/event-stream返回。连接存活期间一直占住一个 waitress 线程,永不释放。web/static/js/app.js:188new EventSource('/api/events')加载即开;断线自动重连。main.py:92,已临时从 8 调到 64,仅缓解)。每个标签页/重连占 1 线程,池满后所有请求永久排队 → 卡死。改法(推荐:短轮询,根治):
/api/events改成立即返回即关闭的普通接口GET /api/events?since=<seq>,一次性返回since之后的新事件 JSON,不再while True。EventSource,改setInterval(fetchEvents, 2000),带上次seq。main.py的threads可恢复到合理值(修好后不应再依赖 64 这个临时缓解);若保留 SSE 方案则必须保证 SSE 不占数据库连接、且标签页隐藏时eventSource.close()、重连指数退避——但短轮询更省心,优先短轮询。泄漏 B — 读接口不结束事务,连接卡在 idle in transaction
这是
/api/stats实测 3/3 超时 25s 的真正原因,和泄漏 A 是同一类问题的两个面。db/database.py:118连接autocommit=False,每线程一条、长期复用、从不关闭(get_conn()线程局部)。db/database.py:825/835的fetchone/fetchall只execute、从不commit/rollback。纯读请求(看板绝大多数接口)跑完第一个 SELECT 后,事务一直开着不关。idle in transaction连接(线上实测约 12 条),占连接槽、持旧快照。线程调到 64 后最多能攒 64 条卡住的连接 → 顶满连接数/卡锁 → 落到冷线程的/api/stats永久排队。改法(推荐:请求结束统一释放事务):
web/app.py加@app.teardown_request,请求结束时db.rollback()(包 try/except,鲁棒)。读路径本就没东西可丢,写路径返回前已commit(),此处 rollback 是空操作。一个钩子即可清掉所有 idle-in-transaction。get_conn()复用前可加一次健康检查或异常时重建)。Part 2 · 首页指标悬停说明(前端,小改)
首页 9 张指标卡只有名字没有解释,用户会误读(尤其"文献"显示的是已处理数不是入库总数;"研究洞见"和"深度发现"是两张不同的表)。给每张卡加
title悬停说明,走现有 i18n(data-i18n-title,applyI18n已支持)。字段含义已逐项核对(
db/database.py:get_stats()+app.js:165-173绑定),按下表加 tip(新增 i18n 键,en/zh 键集合必须相等):statPaperspapers_processedstatResultsresults_totalstatTaxonomytaxonomy_nodes_totalstatContradictionscontradictions_totalstatInsightsinsights_totalstatTokenstokens_consumedstatExperimentsexperiment_runs_totalstatDeepDiscoveriesdeep_insights_totalstatCompletePaperssubmission_bundles_total实现:在
web/templates/index.html每张.stat-card上加data-i18n-title="overview.xxx.tip"(或在卡内 label 上),在web/static/js/i18n.js的 en/zh 各加对应*.tip键。Allowed / Forbidden
web/app.py(加 teardown、改 /api/events)、main.py(threads 值)、db/database.py(连接健康/rollback 相关,仅限本任务所需)、web/static/js/app.js(SSE→轮询)、web/templates/index.html+web/static/js/i18n.js(tip)、相关测试。agents//orchestrator//contracts/的业务逻辑;不引前端框架;不删 tab/功能;不碰数据库 schema。执行约定
Test Execution Protocol
pytest tests/test_web_app.py -q验收(可机验 + 线上复核)
Part 1:
SELECT count(*) FROM pg_stat_activity WHERE state='idle in transaction'→ 归零(或个位且不随请求数增长)。/api/stats在有其它长连接/并发读时,p95 < 1s、稳定 200(不再 25s 超时)。python -c "import web.app"ok;pytest tests/test_web_app.py -q全绿;Flask 起来后/、/api/stats、/api/events(或新?since=)均 200。Part 2:
5.
i18n.js新增 9 个*.tip键,en/zh 键集合仍完全相等(沿用现有 parity 测试)。6. 9 张卡都有可见
title/tooltip,文案与上表一致;切换 EN/ZH 时 tip 跟随。实现红线: idle-in-transaction 必须真归零(不是把阈值调大蒙混);
/api/stats必须真快(不是加 cache 把旧值返回了事——除非明确实现并说明 cache 策略);tip 的 en/zh 键必须对齐。Non-goals
重部署
host 上
git fetch && git checkout <分支>→sudo systemctl restart deepgraph-web→ 复核/api/stats秒回、多标签不卡死、指标卡有悬停说明。