Press DevTools DB Analyzer 数据加载修复

TL;DR

Press DevTools 的 DB Analyzer / SQL Playground 选中站点后一直转圈,是因为后台 Agent Job 类型 Fetch Database Table Schemadisabled_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.pyfetch_database_table_schema() L3724创建 Agent Job,设置 Redis status=1 (loading)
press/press/doctype/agent_job/agent_job.pyAgent class L44Agent Job 的创建/投递/轮询
同上poll_pending_jobs() L598每 5 分钟扫描所有 server 的待处理 Job
同上retry_undelivered_jobs() L794将 Undelivered Job 重新投递给 agent
press/press/doctype/agent_job_type/agent_job_type.pyget_retryable_job_types_and_max_retry_count() L36查询哪些 Job 类型可以自动重试
press/api/client.pysearch_link() L389DevTools 下拉框的数据源 API
同上get_list_query() L195带 role_guard + team 过滤的查询
press/guards/role_guard/__init__.pyis_restricted()角色权限检查
press/utils/__init__.pyget_current_team() L135获取当前用户的 Team
dashboard/src/pages/devtools/database/DatabaseAnalyzer.vueresources.databaseTableSchema前端调 API 的入口
dashboard/src/components/navigation/sidebar/NavList.vueL97-134DevTools 侧边栏菜单定义

流程简述

  1. 用户在 DB Analyzer 选择站点 → 前端调 run_doc_method → 后端执行 Site.fetch_database_table_schema()
  2. 方法检查 Redis 缓存状态:None = 从未加载,1 = 正在加载,2 = 已加载。如果不存在未过期的 Agent Job,创建新的 Agent(self.server).fetch_database_table_schema(...)
  3. Agent Job 创建后状态为 Undeliveredpoll_pending_jobs 每 5 分钟轮询所有 server
  4. retry_undelivered_jobs 调用 get_retryable_job_types_and_max_retry_count() 查询 [[Job|Agent Job]] Type 表——只处理 disabled_auto_retry=0max_retry_count>0 的类型
  5. 阻塞点Fetch Database Table Schema 在表中的 disabled_auto_retry=1,轮询永远跳过它
  6. 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_linkrole_guardALLOWED_DOCTYPES,全是无用功
  • 如果再来一次,我会先打开浏览器 F12 看 Network 面板的实际请求和返回,而不是直接从后端代码开始猜
  • 这个问题的本质其实是 Agent Job 投递配置——一个数据库字段值阻塞了整条链路,不是权限、不是网络、不是 agent 不在线
  • 以后排查类似问题,第一时间应该看的三个东西:1) Agent Jobstatus 字段(是不是 Undelivered) 2) [[Job|Agent Job]] Type 表里对应类型的 disabled_auto_retry 3) ping_agent() 确认 agent 是否在线

🕳️ 踩过的坑 & 被否决的方案


🩺 故障排查决策树

症状 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]] 状态变为 Successjournalctl -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 也是同一流程