4 月 2 日,beads 发布了 v1.0.0。核心特性是 embedded Dolt:一个零配置后端,在进程内运行数据库,无需单独管理服务器。对于独立开发者来说,这意味着 bd init 就搞定了。没有端口,没有守护进程,没有配置。
我们立即开始在 Beadbox 中添加支持。六个热修复版本、一次公开回滚、深入研究 bd 源代码之后,我们带着一个可能几个月前就该构建的弹性层走了出来。
一切崩溃前的早晨
那天开始得很顺利。我们在整个代码库中进行了死代码清理,发布了 v0.20.0,删除了 5,350 行代码,冷启动改善了 2 秒。关闭了四十二个 bead。一个好早晨。
然后我们将 bd 升级到了 0.63.3,这是基于 beads v1.0.0 的 embedded Dolt 后端构建的第一个版本。
Beadbox 找不到数据库了。Embedded 模式将数据存储在 .beads/embeddeddolt/ 而不是 .beads/dolt/。数据库名也变了,从硬编码的 beads 变成了从 metadata.json 读取的项目前缀。而我们的 WebSocket 服务器用于通过 DOLT_HASHOF_TABLE 进行 O(1) 变更检测的 bd sql,在 embedded 模式下完全不支持。
前十分钟,三个假设全部打破。
一天六个版本
发现、修复、发布、再发现。
v0.20.1 添加了使用 OS 钥匙串的凭据持久化(六个 bead 的工作已在进行中),修复了自定义状态过滤器 bug,并修补了 Windows 特定问题。
v0.20.2 教会了 Beadbox 从 metadata.json 读取 dolt_database,以便找到重命名后的数据库。
v0.20.3 添加了 embedded 模式守卫。每个 bd sql 调用都包裹了检查:如果处于 embedded 模式,回退到基于 CLI 的轮询而非直接 SQL 查询。getDoltDir 函数学会了先检查 embeddeddolt/。
v0.20.4 修复了 embedded 布局的 --db 路径规范化。在旧目录结构下能用的路径在新结构下坏了。
每次修复都暴露出下一个问题。
Flock
v0.20.4 之后,我们以为稳定了。然后跑了一个简单的并发测试:五个 bd list 同时调用。
四个失败了。
Embedded Dolt 在每个命令的整个生命周期内获取数据库的排他文件锁(flock)。从 PersistentPreRun 到 PersistentPostRun,其他任何东西都无法访问。这是设计如此。没有它,并发引擎初始化会导致 nil-pointer panic(beads#2571)。Flock 防止了崩溃。但这也意味着在 embedded 模式下,bd 实际上是单进程的。
Beadbox 不是单进程的。我们的 WebSocket 服务器每秒轮询变更。UI 在页面加载时触发多个 server action。用户在后台轮询器运行时点击应用会产生并发的 bd 调用。Flock 会阻塞除第一个之外的所有调用。
DoltHub 关于 embedded 实现的博客文章描述了预期行为:并发调用者应该"通过指数退避自然排队"。但 arch 审查了发布的源代码,发现 bd 使用 TryLock 配合 LOCK_NB(非阻塞)。它不等待。它立即失败。有两层锁:上层是 bd 的 flock,下层是 Dolt 的驱动级退避。第一层短路了第二层。重试逻辑存在于代码库中,但从未执行,因为 flock 在 Dolt 的退避有机会运行之前就拒绝了连接。
修复方案(通过 FlockSharedNonBlock 实现读操作的共享锁)存在于 bd 的源代码中。只是还没有接入。
我们回滚了
我们可以继续对着移动目标发布热修复,也可以退后一步构建正式的弹性层。我们选择了退后。
所有 v0.20.x 版本从公共仓库下架。v0.19.0 重新成为推荐版本。我们发布了一个讨论说明发生了什么以及该怎么做,并在 beadbox.app 上添加了横幅。从决定到完成,三十分钟。
坏版本每多公开一小时,就多一个人下载它、遇到 flock 问题、然后怪罪产品。我们宁愿解释一次回滚,也不愿帮别人调试糟糕的首次体验。
不只是我们
在我们调试的同时,一个叫 Kevin 的 beads 用户发布了 beads#2938:"Beads feels painful to use." 他花了 9.5 小时调试的问题中,就包括我们正在遭遇的 embedded 转 server 的困惑。升级到 v1.0.0 悄悄地将他的工作区从 server 模式切换到了 embedded 模式(beads#2949),把他现有的 issue 藏在了一个全新的空数据库后面。
9.5 小时。一个有经验的用户,不是新手。如果熟悉 beads 的人都是这种体验,问题不在用户。在迁移路径。
我们为 v0.21.0 构建了什么
不再逐个修补失败,我们构建了一个将锁竞争视为正常运行条件的层。
带指数退避的 flock 重试。 每个 bd CLI 调用最多重试 5 次,间隔 100ms 到 1.6 秒。放在 lib/bd.ts 一个位置,所有命令免费获得。覆盖常见场景:两个调用冲突,一个短暂等待,两个都成功。
优雅降级 UI。 锁竞争不再意味着错误屏幕。应用显示过期数据并带有刷新指示器。如果竞争持续超过 30 秒,琥珀色横幅说明情况。锁释放后,横幅消失,数据自动刷新。
自动提升建议。 反复出现的竞争会触发迁移到 server 模式的建议:备份、用 --server 重新初始化、恢复。一键完成。这是在 Beadbox 旁边运行其他 bd 消费者的正确答案,现在应用会主动告诉你,而不是让你自己摸索。
Embedded 模式检测。 getDoltDir 检查 embeddeddolt/ 并相应路由。bd sql 调用受到保护。WebSocket 管道在 embedded 模式下回退到基于 CLI 的轮询(较慢,但尊重单进程约束)。
我们学到了什么
Embedded Dolt 在设计上是单进程的。 不是 bug。Flock 防止真正的 panic。任何并发消费 beads 工作区的工具都需要序列化访问或运行 server 模式。对 Beadbox 来说,server 模式是正确的默认值。Embedded 适合轻度使用,重试层可以吸收偶尔的冲突。
文档描述的是意图,不是实现。 DoltHub 博客说退避。代码说 TryLock 配合 LOCK_NB。我们花时间假设并发读应该可以工作,因为文档是这么说的。读源代码几分钟就解决了困惑。当行为和文档不一致时,读代码。
发布前测试并发。 直到 v0.20.4 公开后我们才运行并发 bd 调用。for i in {1..5}; do bd list & done; wait 会在任何发布之前发现 flock 问题。五秒钟的测试就能省去一次回滚。
尽早回滚。 继续前进的本能很强。你很接近了,能看到修复,再来一个版本。但每个留在公开状态的坏版本都是一笔不容易撤销的信任透支。回到 v0.19.0 给了我们空间来正确构建弹性层,而不是在恐慌中一点点发布。
检查环境变量。 我们因为 BEADS_DIR 指向了错误的工作区而浪费了好几个小时。bd 发现的数据库和 Beadbox 监控的不是同一个,症状看起来像数据损坏。如果你的 bd 命令返回意外结果,在做任何事之前先 env | grep BEADS。
当前状态
v0.21.0 已发布,包含 beads v1.0.0 支持、弹性层以及通过 OS 钥匙串的凭据持久化。发布讨论中有完整细节。
如果你在 beads v1.0.0 embedded 模式下遇到间歇性故障,v0.21.0 的重试层应该能处理。如果你在 Beadbox 旁边运行其他访问同一工作区的工具,请切换到 server 模式。自动提升流程一键搞定。
如果你是 Steve 或 beads 团队中正在读这篇文章的人:读操作的共享 flock 会从上游修复根本原因。beads#2939(Unix domain sockets)也会让本地连接更干净。不管发布什么,我们会继续围绕它来构建。
亲自试试
先用 beads 作为协调层。需要可视化管理时再加上 Beadbox。
Beta 期间免费。无需注册账号。原生支持 Dolt。