Steven
Steven1 分钟阅读

一个真正的第二版需要付出什么: 206 次提交换来的诚实状态

GeekBye v2 不是一次功能发布。它是瞄准一个念头的 206 次提交: 应用永远不该对自己的状态说谎。这需要付出什么,我们写在这里 — 包括那个差点让我们什么都发不出去的、一行锁文件的失误。

可靠性
工程
发布
GeekBye 发布
一个真正的第二版需要付出什么: 206 次提交换来的诚实状态

大多数"第二版"发布,都是一堆贴着更大数字的新功能。GeekBye v2 恰恰相反。它几乎没有交付任何面向用户的新能力。它的 206 次提交瞄准的是一个单一的、并不光鲜的念头:

应用不该给你看一个不真实的状态。

这听起来理所当然 — 直到你数一数一个桌面应用有多少种悄悄说谎的方式。它在一个仍在传输中的上传上打了个勾。它在一个一分钟前就死掉的套接字上说"已连接"。它在你的会议记录被丢到地上时保持沉默。这些都不是崩溃。它们比崩溃更糟,因为应用明明是错的却看起来一切正常 — 而你只会在之后才发现,当你需要的那段录音并不在那里的时候。

v2 就是我们出发去猎捕每一个这种小谎言的那次发布。

我们最恨的那个谎言: "成功了"的上传

GeekBye 可以把你的录音备份到 Google Drive。在 v1 里,与 Drive 的连接被当作一个背景细节 — 成了最好,不成,那次失败大多是隐形的。应用会继续看起来是连着的。你会以为录音是安全的。有时候它们并不安全。

v2 围绕诚实的连接状态重建了这一切。Drive 现在总是处于几个明确、真实的状态之一 — 已连接、正在重连、已断开 — 而且状态一有变化,应用就在那一刻把这个变化广播给整个界面。当连接断开时,一个全局重连横幅会在任何地方、明明白白地告诉你。应用不再在上传没成功的时候假装成功。如果你的备份现在做不了,你现在就知道 — 而不是等到下周你去找那个文件的时候。

在底层,这是一个小小的架构,而不是一句口号: 关于连接的唯一真相来源,一个在它变化时扇出到 UI 每个部分的事件,以及一个直接从那个状态读取的横幅。不存在这样一条路径: 连接断了而 UI 看起来是连着的,因为它们读的是同一个事实。

丢失录音的那个谎言

第二个重大的诚实修复是录音恢复。实时转录需要一个活着的连接。在 v1 里,如果那个连接在会话中途打了个嗝 — Wi-Fi 一闪、过隧道、VPN 重连 — 那段间隙里的音频可能就直接没了。会议记录里会有个洞,而没有任何东西告诉你。

v2 改了契约: 音频在连接中断期间被保存在本地,并在之后对账。 当链路掉线时,GeekBye 把音频安全地留在你的机器上;当它恢复时,那段缓冲的音频被发送出去,并在正确的位置缝进会议记录。糟糕的三十秒网络,不再让你损失三十秒的会议。这就是后来那些可靠性发布直接在其上构建的地基 — 为什么你的 AI 会议记录工具在糟糕的 Wi-Fi 下会停里那套重连并缓冲的机制,就是同一个念头,被加固过。

弹窗风暴的那个谎言

应用报告麻烦的方式里,藏着一种更微妙的不诚实: 它过度报告。一个不稳定的网络时刻能把同一个错误触发五次,突然之间你就有了一摞一模一样的红色提示,把那条真正重要的消息埋了起来。那也不诚实 — 那是伪装成信息的噪声。

于是 v2 加入了按类别键控的提示限流和错误路由。 错误按它们实际是什么来分组,每个类别都被限速,于是一个底层问题产生的是一条清晰的消息,而不是一阵猛烈的连发。限速问题路由到限速消息;连接问题路由到连接消息。应用告诉你什么是真的,一次 — 正是这份纪律,后来在我们的应用 DDoS 了自己的那天里,阻止了一个 429 冒充成登出。

206 这个数字才是重点

诚实的状态、重连横幅、录音恢复、提示路由 — 这是四个想法。而这次发布是 206 次提交。剩下的都去哪了?

去了"绝不显示一个虚假状态"真正需要的那条不光鲜的长尾。旧 UI 每一个可能与现实脱节的地方,都必须被找出来,重新接线,让它从真相读取,而不是从一份陈旧的副本。每一条重连路径,都必须被改成去更新那个共享状态,而不是在本地猜测。横跨录音、转录和上传的几十个小小的可靠性修复 — 每一个都堵上了一个具体的缝隙,在那里应用本可以看起来对却其实错了。

这就是当目标是信任而非功能时,一个真正的"第二版"要付出的代价。没有单独一个头条式的提交。有的是 206 个小提交,而这次发布之所以感觉像是一件事 — 平静 — 是因为这 206 个全都朝着同一个方向拉。

这次发布差点没能上线

这里有个战斗故事,而且是个好故事,因为它讲的是我们自己的应用对我们说谎。

当你切一次发布时,一个版本号更新脚本会把新数字盖到整个项目上。在 v2.0.0 上,它更新了应用的版本 — 但 package-lock.json,npm 那份精确的依赖账本,却被留在指向版本 1.9.0。在本地,一切都好;没人为了构建去重装依赖。但 CI 会运行 npm ci,而 npm ci 的全部职责就是如果锁文件与清单不一致,就拒绝继续。这是一个严格性特性 — 而它做了它该做的事,方法是让我们切过的最大一次发布的构建失败。

然后,在它底下又浮出来一个更狡猾的第二个问题。一个更新版本的 npm 把一个可选的传递依赖给剪掉了 — 就是 node-fetch 拉进来的那个 encoding 包 — 当作不必要的,从锁文件里剪掉了。可我们 CI 的干净安装确实需要它,于是安装以一种和我们的代码毫无关系、而完全在于账本微妙出错的方式崩了。

两个都是一行的修复: 把锁文件重新同步到真正的版本,恢复那个被剪掉的条目。两个也都是 v2 所讲的那件事的完美而令人谦卑的例子 — 一个声称为真、其实不真的状态。 锁文件本该是应用依赖什么的诚实记录。当它偏离现实时,构建做了我们的应用如今为用户所做的一模一样的事: 它拒绝假装一切都好。原来,交付一次可靠性发布,从头到脚都是一个可靠性问题。

v2 教给我们的三件事

  1. 危险的失败是那些沉默的。 崩溃会自报家门。一个假的"已连接"、一个幻影般的上传成功、一份带着隐形洞的会议记录 — 它们之所以让你损失信任,正是因为没有任何东西看起来出了错。去猎那些安静的谎言。
  2. 诚实是一种架构,不是一条消息。 你没法把"说真话"当作一个横幅拴到 UI 上。这个横幅必须和其他一切从同一个唯一真相来源读取,否则它就变成又一个可能出错的东西。一个事实,扇出去 — 绝不要两份可能互相矛盾的副本。
  3. 配得上那个数字的第二版,大部分是看不见的。 如果你的 v2 是一张功能清单,那它是一个带营销的 v1.5。一个真正的 v2 是 206 次没人能单独指出来的提交,加起来成为一个就是不再对你说谎的产品。

GeekBye v2.0.0 是此后每一次发布都在其上构建的地基。关于这个地基承载了什么,请看为什么你的 AI 会议记录工具在糟糕的 Wi-Fi 下会停我们的应用 DDoS 了自己的那天(v2.0.1),以及当防火墙拦住 WebSocket 时的实时转录(v2.0.8)。至于这一切所服务的那份平静,请看 GeekBye v2 有什么新东西