Press DevTools DB Analyzer 数据加载修复
TL;DR
Press DevTools 的 DB Analyzer / SQL Playground 选中站点后一直转圈,是因为后台 Agent Job 类型
Fetch Database Table Schema的disabled_auto_retry默认为1,导致轮询机制跳过不投递。改为0后 gottenergy 和 demo1 恢复正常。现状:✅ gottenergy + demo1 + demo2 全部正常。首次加载慢是正常的(Agent 需连 MariaDB 查表结构),之后切换丝滑(Redis 有缓存)。Cloud 也是同一流程,不额外加按钮
🎯 背景:为什么需要这个
- 需求:在 Press UI 的 Dev Tools 里查看站点数据库表结构、执行 SQL 查询
- 卡点:选中站点后页面一直转圈显示 “加载表模式”,永远不完成
- 最初误判:以为是 DevTools 下拉框空白、
public=1配置问题——实际下拉框正常,问题在数据加载链路 - 硬性约束:不改 Frappe/Press 核心框架代码,只做配置级修复
✅ 前置条件
- Press 控制面板可访问
- Press 容器内
bench --site os.getgrowth.app execute可用 - f1 agent 在线(
ping_agent()返回pong) - 知道目标站点的
server字段值(f1.getgrowth.app)
🏗️ 最终架构
正常路径(首次投递成功,不走轮询):
Press UI 选择站点
→ 前端调 run_doc_method
→ Site.fetch_database_table_schema() 创建 [[Job|Agent Job]]
→ Agent 立即投递到 bench 容器
→ bench agent 连 MariaDB 查表结构 → 结果写 Redis → Redis status=2
→ 前端轮询读到 status=2 → 渲染表结构
兜底路径(首次投递失败时):
[[Job|Agent Job]] 状态保持 Undelivered
→ poll_pending_jobs (每 5 分钟)
→ retry_undelivered_jobs
→ 检查 [[Job|Agent Job]] Type.disabled_auto_retry
→ = 0:重新投递
→ = 1:跳过(我们之前的卡点)
和 Cloud 的区别:Cloud 的 Press→Agent 通信在同一 VPC 内网,立即投递几乎不失败,所以 disabled_auto_retry=1 也不影响。我们的跨网络通信偶尔不稳定,需要轮询兜底。
和一开始想的区别:不是前端 UI 配置问题(public=1),是 Agent Job 投递链路的配置字段阻塞。
🧠 涉及到的代码逻辑
关键文件路径 + 核心方法名:
| 文件 | 关键方法/行号 | 做了什么 |
|---|---|---|
press/press/doctype/site/site.py | fetch_database_table_schema() L3724 | 创建 Agent Job,设置 Redis status=1 (loading) |
press/press/doctype/agent_job/agent_job.py | Agent class L44 | Agent Job 的创建/投递/轮询 |
| 同上 | poll_pending_jobs() L598 | 每 5 分钟扫描所有 server 的待处理 Job |
| 同上 | retry_undelivered_jobs() L794 | 将 Undelivered Job 重新投递给 agent |
press/press/doctype/agent_job_type/agent_job_type.py | get_retryable_job_types_and_max_retry_count() L36 | 查询哪些 Job 类型可以自动重试 |
press/api/client.py | search_link() L389 | DevTools 下拉框的数据源 API |
| 同上 | get_list_query() L195 | 带 role_guard + team 过滤的查询 |
press/guards/role_guard/__init__.py | is_restricted() | 角色权限检查 |
press/utils/__init__.py | get_current_team() L135 | 获取当前用户的 Team |
dashboard/src/pages/devtools/database/DatabaseAnalyzer.vue | resources.databaseTableSchema | 前端调 API 的入口 |
dashboard/src/components/navigation/sidebar/NavList.vue | L97-134 | DevTools 侧边栏菜单定义 |
流程简述:
- 用户在 DB Analyzer 选择站点 → 前端调
run_doc_method→ 后端执行Site.fetch_database_table_schema() - 方法检查 Redis 缓存状态:
None= 从未加载,1= 正在加载,2= 已加载。如果不存在未过期的 Agent Job,创建新的Agent(self.server).fetch_database_table_schema(...) - Agent Job 创建后状态为
Undelivered。poll_pending_jobs每 5 分钟轮询所有 server retry_undelivered_jobs调用get_retryable_job_types_and_max_retry_count()查询[[Job|Agent Job]] Type表——只处理disabled_auto_retry=0且max_retry_count>0的类型- 阻塞点:
Fetch Database Table Schema在表中的disabled_auto_retry=1,轮询永远跳过它 - Agent 在 bench 上执行 MariaDB 查询 → 结果写 Redis → Press 前端轮询 Redis status=2 → 渲染
关键细节:
disabled_auto_retry=1是默认值,意味着这个 Job 类型设计为”创建时立即投递”而非”靠轮询兜底”。但实际创建时投递可能失败(agent 通信延迟等),需要轮询做兜底。
🛠️ 部署步骤
第 1 步:确认 agent 在线
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app execute "frappe.get_doc('Server','f1.getgrowth.app').ping_agent()"预期输出:pong
第 2 步:查看当前 Undelivered Job
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app execute "frappe.get_all('[[Job|Agent Job]]', filters={'job_type': 'Fetch Database Table Schema'}, fields=['name','status','site'], order_by='creation desc', limit=10)"如果全部 Undelivered,说明轮询在跳过。
第 3 步:修复 — 启用 auto retry + 清旧 Job + 重触发
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app console# 启用 auto retry
frappe.db.set_value('[[Job|Agent Job]] Type', 'Fetch Database Table Schema', 'disabled_auto_retry', 0)
# 删旧 Undelivered Job
jobs = frappe.db.sql("SELECT name FROM `tabAgent Job` WHERE job_type='Fetch Database Table Schema' AND status='Undelivered'", as_dict=True)
for j in jobs:
frappe.delete_doc('[[Job|Agent Job]]', j.name)
# 重触发
frappe.get_doc('Site', 'demo2.getgrowth.app').fetch_database_table_schema(reload=True)第 4 步:手动触发轮询(不等 5 分钟)
from press.press.doctype.agent_job.agent_job import poll_pending_jobs
poll_pending_jobs()
import time; time.sleep(5)
# 检查结果
jobs = frappe.get_all('[[Job|Agent Job]]', filters={'job_type': 'Fetch Database Table Schema'}, fields=['name','status','site'], order_by='creation desc', limit=5)
print(jobs)
print(frappe.cache().get_value('database_table_schema__status:demo2.getgrowth.app'))↩️ 回滚 / 卸载
- 回滚命令:
frappe.db.set_value('[[Job|Agent Job]] Type', 'Fetch Database Table Schema', 'disabled_auto_retry', 1) - 需要清理的数据:无(只是改了配置字段)
- 回滚后验证:创建新的 Undelivered Job 后等 5 分钟,确认它仍未被投递
🔍 验证方法
在 Press UI → Dev Tools → DB Analyzer → 选择站点(如 gottenergy.getgrowth.app)→ 应该看到表结构加载完成。
如果一直转圈:F12 看 Network 面板 → 搜 run_doc_method → 看返回是 loading: true 还是报错。
💡 总结经验
- 这次最大的教训:不要假设问题是什么——先确认症状。下拉框空白 ≠ DB 加载卡住,两条完全不同的代码路径。我花了大量时间查
search_link、role_guard、ALLOWED_DOCTYPES,全是无用功 - 如果再来一次,我会先打开浏览器 F12 看 Network 面板的实际请求和返回,而不是直接从后端代码开始猜
- 这个问题的本质其实是 Agent Job 投递配置——一个数据库字段值阻塞了整条链路,不是权限、不是网络、不是 agent 不在线
- 以后排查类似问题,第一时间应该看的三个东西:1) Agent Job 的
status字段(是不是 Undelivered) 2)[[Job|Agent Job]] Type表里对应类型的disabled_auto_retry3)ping_agent()确认 agent 是否在线
🕳️ 踩过的坑 & 被否决的方案
方向 A:怀疑 DevTools 下拉框是
public=1配置问题(误判)问题:花了几十轮调试
search_linkAPI、role_guard、get_list_query、ALLOWED_DOCTYPES、get_current_team——全部走错方向 证据:用户打开页面后发现下拉框完全正常,只是选中站点后加载转圈 教训:先确认问题描述是否准确,打开浏览器看一眼比看代码快 100 倍
坑 1:
Fetch Database Table SchemaJob 全部 Undelivered现象:DB Analyzer 永远 loading,Agent Job 状态全是
Undelivered根因:[[Job|Agent Job]] Type表中该类型的disabled_auto_retry=1,轮询跳过 解决:frappe.db.set_value('[[Job|Agent Job]] Type', 'Fetch Database Table Schema', 'disabled_auto_retry', 0)
坑 2:demo2 的 Redis 状态卡在 1(loading)→ 已自愈
现象:gottenergy 和 demo1 修复后正常,demo2 仍然转圈 根因:轮询投递有延迟,Agent Job 成功后 Redis 回调还没完成 解决:等了一会儿自己好了。首次加载完成后 Redis 有缓存,后续切换丝滑
坑 3:
bench execute反引号表名在 shell 链中全碎现象:
\tabAgent Job`经过docker exec → ssh → bench execute后变成空字符串 **根因**:bash 在多层嵌套中逐层剥离反引号/反斜杠 **解决**:复杂操作改用bench console` + heredoc,避免 SQL 字符串转义
🩺 故障排查决策树
症状 1:DB Analyzer 选中站点后一直转圈
→ 查 [[Job|Agent Job]] 状态(命令:frappe.get_all('[[Job|Agent Job]]', filters={'job_type': 'Fetch Database Table Schema'}, ...))
→ 如果全是 Undelivered → 查 [[Job|Agent Job]] Type disabled_auto_retry 是否为 1
→ 如果是 Pending/Running → 等 1 分钟再查,可能 agent 正在执行
症状 2:[[Job|Agent Job]] 状态是 Success 但仍然转圈
→ 查 Redis(命令:frappe.cache().get_value('database_table_schema__status:站点名'))
→ 如果是 1 → agent 回调没更新 Redis,删 status key 后 re-trigger
→ 如果是 None → 前端没触发过加载,re-trigger 一次
→ 如果是 2 → 前端缓存问题,硬刷新页面
症状 3:Agent 离线
→ ping_agent() 不返回 pong → 去 n1/f1 VPS 查 agent 进程和 nginx 配置
⚠️ 已知限制 / 副作用
disabled_auto_retry=0增加了轮询负载:每次poll_pending_jobs都会尝试投递这类 Job。但max_retry_count=1限制了重试次数,影响很小- demo2 的 Redis 回调问题未解决:如果其他站点也出现 Job 成功但 Redis 没更新,需要查 agent callback 链路
- 容器重建后
disabled_auto_retry不会丢失:这是 DB 中的 Agent Job Type 记录,不是容器内文件。但 bench update 可能重置这个值 - 如果有其他 Job 类型也有类似问题:直接查
[[Job|Agent Job]] Type表,看哪些disabled_auto_retry=1且实际需要轮询投递的
🚨 监控 / 告警
- 健康检查命令:
frappe.get_all('[[Job|Agent Job]]', filters={'job_type': 'Fetch Database Table Schema', 'status': 'Undelivered'})— 预期返回少量(<5),如果堆积超过 10 个说明轮询又跳过了 - 日志关键字:成功时
[[Job|Agent Job]]状态变为Success;journalctl -u cf-smtp-bridge与此无关(这是邮件的) - 自动恢复:
poll_pending_jobs每 5 分钟自动重试,不需要人工干预 - 如果彻底挂了:用户会在 Press UI 发现 DB Analyzer 加载不出来
🔒 安全 & 成本速查
- 涉及的生产数据:无。只修改了
[[Job|Agent Job]] Type表的一条配置记录 - 谁能访问:只有 Press 管理员能触发 DB Analyzer 加载
- 月度成本:无额外成本
- 如果要下线:把
disabled_auto_retry改回1,清掉 Undelivered Job 即可
📌 待办 / 未来优化方向
- 检查还有哪些 Agent Job Type 的
disabled_auto_retry=1但实际上需要轮询兜底 - 优化 Press→Agent 通信稳定性,减少首次投递失败(根本解决办法,不需要改轮询配置)
📋 速查命令合集
# 查 [[Job|Agent Job]] 状态
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app execute "frappe.get_all('[[Job|Agent Job]]', filters={'job_type': 'Fetch Database Table Schema'}, fields=['name','status','site'], order_by='creation desc', limit=5)"
# 查 [[Job|Agent Job]] Type 配置
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app execute "frappe.get_all('[[Job|Agent Job]] Type', filters={'name': 'Fetch Database Table Schema'}, fields=['name','disabled_auto_retry','max_retry_count'])"
# 查 Redis schema 状态
docker exec -i $(docker ps -qf "name=press-backend") bench --site os.getgrowth.app execute "print(frappe.cache().get_value('database_table_schema__status:demo2.getgrowth.app'))"
# 手动触发轮询(bench console 内)
from press.press.doctype.agent_job.agent_job import poll_pending_jobs
poll_pending_jobs()📝 更新日志
| 日期 | 改动 | 原因 |
|---|---|---|
| 2026-07-08 | 首次创建,完成 Agent Job 投递修复 | P1-2 DevTools 数据加载卡住 |
| 2026-07-08 | 三个站点全通,修正架构描述(正常路径 vs 轮询兜底),移除错误的新增按钮建议 | demo2 自愈,确认 Cloud 也是同一流程 |