Beads 性能修复 be-1he 实战:消除多数据库 Dolt 服务端根目录下 bd 命令的 12 秒慢路径
Beads 性能修复 be-1he 实战消除多数据库 Dolt 服务端根目录下 bd 命令的 12 秒慢路径【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads本篇技术指南围绕 Beads 仓库中scripts/repro-be-1he-slow-path/REPRO.md所记录的 be-1he 性能修复展开剖析一个真实且隐蔽的性能问题当工作区运行多数据库multi-DBDolt server 时其服务端根目录.beads/dolt/下只有dolt sql-server写出的.dolt/sql-server.info、却没有.dolt/repo_state.json此时任何触发dolt remote -v子进程的路径都会卡住约 12 秒后才失败。读完本文你将掌握如何复现这个慢路径、它为什么存在、be-1he 通过「按目录状态选择超时上限 只读版本探针」两层修复如何兜底以及如何在真实工作区中验证修复效果。问题背景一条 12 秒的隐性命令延迟Beads 是一个给编码 Agent 提供「记忆升级」的命令行工具底层用 Dolt 存储。在 be-1he 修复之前某些bd命令会出现约 12 秒的异常延迟触发条件组合相当隐蔽.beads/.local_version中记录的 bd 版本字符串与当前二进制不一致陈旧版本标记bd的PersistentPreRun钩子因此触发autoMigrateOnVersionBump自动版本迁移迁移流程需要以可写方式打开存储并调用syncCLIRemotesToSQL → ListCLIRemotes → dolt remote -v在 Dolt 服务端根目录上执行dolt remote -v子进程多数据库服务端根目录的结构是「有.dolt/sql-server.info、无.dolt/repo_state.json」dolt remote -v在这种目录下需要约 12 秒才失败报错fatal: The current directorys repository state is invalid. open .dolt/repo_state.json: no such file or directory需要特别说明的是文档中的「历史性框架」syncCLIRemotesToSQL与migrateServerRootRemotes在当前的main分支上已不存在——那次可写打开路径被单独重构为基于doltutil.PersistedRemotes的快速磁盘读取直接读repo_state.json不再起dolt子进程。但ListCLIRemotes本身仍然存在且依然会 shell 出去执行dolt remote -v其调用点包括 cmd/bd/doctor/federation.gofederation 健康检查以及 CLI push/pull/fetch 的远端路由逻辑internal/storage/doltutil/remotes.go。这些仍然存活的调用点正是 be-1he 第 2 层超时保护的对象。复现环境与前置条件复现该慢路径需要满足以下环境条件摘自 REPRO.mdbd二进制为 be-1he 修复前的任意版本doltCLI 在PATH中ListCLIRemotes依赖它多数据库 Dolt server 根目录结构.beads/dolt/下存在.dolt/sql-server.info但不存在.dolt/repo_state.json。官方复现入口是一条脚本./scripts/repro-be-1he-slow-path/repro.sh脚本支持--no-cleanup参数保留临时目录以便事后检查。脚本的核心逻辑见 repro.sh分四步构造损坏的服务端根目录结构、计时裸dolt remote -v、展示 bd 第 2 层超时的选择依据、并用一个正常dolt init出来的仓库做对照。注意脚本直接调用裸的dolt二进制而不是 bd 的ListCLIRemotes因此计时结果永远不会被 bd 的超时上限截断——它演示的是该上限要防住的底层 dolt 慢行为本身。手动复现 12 秒挂起不想跑整条脚本时可以按 REPRO.md 给出的三步手动复现# 1. 构造损坏的 server root 结构 TMPDIR$(mktemp -d) mkdir -p $TMPDIR/.dolt echo [{host:127.0.0.1,port:3307}] $TMPDIR/.dolt/sql-server.info # 注意这里故意不创建 repo_state.json # 2. 观察裸子进程——这是该复现针对的底层 dolt 慢行为。 # 这里直接调用 dolt不走 bd 的 ListCLIRemotes所以没有任何上限约束它。 cd $TMPDIR time dolt remote -v # 约 12 秒后失败 cd - # 3. 观察 bd 如何划定其第 2 层上限repo_state.json 缺失正是选择 # 激进 2 秒超时的条件存在 → 30 秒宽松上限避免把慢但有效的 # 回答误判为远端不存在。 ls $TMPDIR/.dolt/repo_state.json 2/dev/null || echo absent → bds ListCLIRemotes uses the 2s cap here关键洞察在于dolt remote -v对「看起来像仓库但不是仓库」的目录有.dolt/sql-server.info却无repo_state.json需要约 12 秒才报错而 bd 无法控制上游 dolt 的这个行为只能在自己的这一侧对子进程施加墙上时钟上限。在真实工作区验证 bd 命令序列REPRO.md 还给出了在一台运行多数据库 Dolt server 的工作区上的验证序列# 模拟陈旧的 .local_version任何不同的版本字符串都会触发迁移 OLD_VERSION$(cat .beads/.local_version) echo 0.0.0 .beads/.local_version # 计时一条 bd 命令任何会经过 PersistentPreRun 的命令均可 time bd version # 恢复 echo $OLD_VERSION .beads/.local_version历史行为修复前且在main分支的PersistedRemotes重构之前当迁移被触发并在 server root 上调用dolt remote -v时bd version可能耗时约 12 秒。装上 be-1he 的两层修复后ListCLIRemotes对这类目录把同一调用封顶在 2 秒该函数在当前的main上仍可从 doctor federation 检查与 CLI 远端路由触达只是不再走普通自动迁移路径而bd version本身无论何种情况都在 1 秒内返回——因为第 3 层的只读探针在没有迁移需要时避免了可写打开。修复的两层超时上限与只读探针be-1he 实际交付的修复由两层组成对应关系见 REPRO.md 的表格层文件作用2internal/storage/doltutil/remotes.goListCLIRemotes用context.WithTimeout包装dolt remote -v目标目录缺repo_state.json本复现场景时上限 2 秒否则 30 秒——真实的慢但有效的远端列表绝不会被误判为「远端不存在」3cmd/bd/version_tracking.goautoMigrateOnVersionBump在可写打开存储之前先做只读的bd_version探针无迁移需要时跳过不必要的initSchema往返值得强调的历史事实该修复的早期草案还描述过「第 1 层」——在internal/storage/dolt/federation.go中通过migrateServerRootRemotes先 stat 检查repo_state.json再调用ListCLIRemotes的哨兵。这一层从未随 be-1he 发布federation.go在该 PR 的 diff 中没有任何改动因此也不属于本复现所演示的内容。发布门禁文档 release-gates/be-1he-slow-path-fix-gate.md 明确记录了这一纠正cherry-pick 提交9b2d0b87a只改动 2 个文件、净增 22 行删 1 行仅含第 2、3 层。第 2 层源码剖析按目录状态选择超时ListCLIRemotes的核心实现在 internal/storage/doltutil/remotes.golistCLIRemotesTimeoutBroken 2 * time.Second对缺.dolt/repo_state.json的目录已知的「破损父目录」失败模式例如多 DB server root封顶 2 秒。注释明确说明这类目录永远不可能给出真实回答所以快速失败毫无风险——不会把「慢但有效的远端列表」误判为「不存在」。listCLIRemotesTimeoutHealthy 30 * time.Second对存在repo_state.json的真实 Dolt 仓库用刻意宽松的 30 秒。原因写得很清楚FindCLIRemote会把ListCLIRemotes的任何错误包括超时折叠成「远端不存在」而EnsureCLIRemote会基于该信号盲目添加远端如果远端实际存在则会硬失败。真实仓库的dolt remote -v即使在负载下也只有约 130ms30 秒只会命中真正挂死的子进程——注释特别提示评审者「不应收紧到慢但有效的调用可能越过的值」review should-fix, 2026-07-24。listCLIRemotesTimeout(dbPath)是一个纯函数os.Stat检查dbPath/.dolt/repo_state.json存在即健康 30 秒否则破损 2 秒。纯 stat 判断使其调用成本极低且不 shell 出 dolt 即可独立测试。ListCLIRemotes本体则用context.WithTimeout(context.Background(), listCLIRemotesTimeout(dbPath))创建超时上下文再以exec.CommandContext执行固定命令dolt remote -v工作目录设为dbPath解析输出为storage.RemoteInfo列表。对应的单元测试见 internal/storage/doltutil/remotes_test.golistCLIRemotesTimeout必须只在缺repo_state.json时选 2 秒上限。与超时并列的还有同文件中的PersistedRemotes它直接读取dbPath/.dolt/repo_state.json解析远端不 shell 出 dolt CLI因此 dolt 二进制缺失时也能工作且失败模式可区分——.dolt目录或repo_state.json缺失表示「这里不是 dolt 仓库」返回nil, nil文件存在但不可读/不可解析才返回错误让调用方能区分「确定没有」与「无法确定」。测试见 internal/storage/doltutil/persisted_remotes_test.go。它正是main分支上替代历史可写打开迁移路径的快速磁盘探针。第 3 层源码剖析迁移前的只读版本探针autoMigrateOnVersionBump在 cmd/bd/version_tracking.go 中实现其第 3 层逻辑是在dolt.NewFromConfig可写打开之前先用dolt.NewFromConfigWithOptions(ctx, beadsDir, dolt.Config{ReadOnly: true})以只读方式打开存储通过recordedWorkspaceVersion读取版本标记若记录的版本已等于当前Version则直接返回跳过可写打开与initSchema往返。注释特别说明在当前的main上可写打开门禁已通过doltutil.PersistedRemotes读远端不再是dolt remote -v子进程所以这一层省下的不是 12 秒挂起而是无迁移需要时一次多余的 initSchema 往返。该函数的调用链在 cmd/bd/doctor.go 中清晰可见doctor 命令因为跳过了PersistentPreRun的 DB 初始化skipStoreAnnotation所以trackBdVersion()与autoMigrateOnVersionBump()尚未执行需要显式补调——这也印证了两者设计上就是PersistentPreRun生命周期的一部分。版本探测本身由trackBdVersion完成读取.beads/.local_versiongitignored 文件仅当发现升级版本号更高时置位versionUpgradeDetectedautoMigrateOnVersionBump据此决定是否进入迁移体。发布门禁与验证结论be-1he 的发布门禁文档 release-gates/be-1he-slow-path-fix-gate.md 给出了工程验证结论可作为修复效果的依据评审通过Reviewer PASS构建 / vet / lint 全部干净验收标准 2/3 层达成第 2 层context.WithTimeout2 秒上限 命名常量listCLIRemotesTimeout第 3 层可写打开前的只读bd_version探针第 1 层确认不在本次 diff 内测试无回归go test -tags gms_pure_go -count 1 -short在 cherry-pick 分支与origin/main上的失败集合完全一致internal/storage/doltutil/...干净5.0s版本追踪定向测试-run TestAutoMigrate|TestVersion|TestCheckVersion0.107s 通过分支干净仅领先origin/main1 个提交cherry-pick 无冲突。小结与排查启示be-1he 是「慢路径排查 分层防御」的典型样本对读者的实战启示有三点慢行为可能来自上游子进程bd 无法修复 dolt CLI 在非仓库目录上 12 秒失败的上游行为但可以在自己的调用侧用context.WithTimeout兜底把不可控的外部延迟变成可预测的快速失败。超时值要与状态语义绑定2 秒 vs 30 秒的选择不是拍脑袋——缺失repo_state.json意味着「永远不会有答案」可以激进存在则意味着「慢但可能是对的」必须宽松以免把有效回答误判为「远端不存在」进而盲目添加远端导致硬失败。尽量把「探测」做成只读与纯函数listCLIRemotesTimeout是纯 stat 判断、recordedWorkspaceVersion走只读打开、PersistedRemotes直接读盘——三者都让快速失败路径不需要付出打开写库的代价。若你在自己的多数据库 Dolt server 部署中观察到 bd 命令偶发十几秒延迟优先检查.beads/dolt/.dolt/下是否缺失repo_state.json并运行./scripts/repro-be-1he-slow-path/repro.sh对照确认即可快速定位是否属于本类问题。【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考