第 8 / 23 篇

第 7 章:租约允许接手,围栏限制旧执行者

worker 暂时没有心跳,可能已经退出,也可能只是网络暂停、进程被调度器挂起或者发生长时间停顿。另一个 worker 接手后,旧执行者仍可能恢复。可靠执行需要同时处理“允许新执行者继续”和“阻止旧执行者错误裁决”这两件事情。

7.1 租约描述期限,不负责杀死进程

示例在领取时保存 owner 与 lease_until。只要租约有效且拥有者、围栏仍匹配,worker 就有资格提交本地状态。期限到达后,恢复过程可以收回这项资格,处理失去确认的任务。

租约本身不会停止旧进程,也不会撤销已经发送的网络请求。数据库把任务交给别人,并不意味着工具侧已经忘记旧调用。把租约理解成一种自动杀进程机制,会在故障期间造成两位执行者都以为自己可以继续。

应当区分失去本地写入资格与远程效果是否存在。前者可以通过数据库条件更新检查;后者需要工具协议、幂等键、结果查询或者额外的围栏执行机制。任何一层缺失,都需要保留相应的不确定性。

7.2 心跳只能延续当前有效资格

心跳不是“按任务编号把到期时间改到以后”。示例要求任务仍在运行、拥有者相同、围栏相同,而且当前租约尚未过期。超过期限的旧拥有者不能通过一条迟到心跳重新获得权限。

心跳频率、租约长度和工具调用期限需要配合。租约太短,会让正常抖动频繁触发核查;太长,则在进程退出后留下更长等待。选择时应测量部署环境中的暂停和调度行为,并把误判恢复与资源占用一同纳入考虑。

实验使用秒级租约,是为了在可接受时间内真实观察到期,不是给所有生产任务推荐三秒。一个跨数小时的任务并不意味着需要数小时的单次租约;它可以保存长期业务状态,由存活 worker 维持较短执行租约,审批等待期间则不必一直占有运行资格。

7.3 围栏编号让旧结果可以被拒绝

每次有效领取或权限失效都会推进 fence。旧 worker 即使保存着正确工作流编号和原 owner 字符串,也只能携带旧 fence 提交。数据库只接受当前状态、当前拥有者和当前 fence 的组合。

第一次领取:fence = 1
原拥有者失效并进入核查:fence 继续推进
核查后允许重排,再领取:获得更大的 fence
旧进程提交 fence = 1:拒绝

编号不必连续对应业务步骤,读者不应把某个具体数值当作状态含义。它的作用是识别资格世代,防止旧观察者覆盖新决策。相同逻辑也适用于旧核查结果:若并发流程已经改变状态,核查者需要重新读取,而不是继续使用旧快照。

7.4 本地围栏不会自动保护下游

本书的条件更新保护工作流数据库,没有要求工具服务解释 fence。工具侧主要依靠稳定操作键、参数指纹和持久账本防止重复效果。旧调用可能仍然抵达工具,只要它与原批准意图相符,就可能返回已有回执。

如果工具必须拒绝过期写入,工具自身也需要理解围栏,并明确比较与更新的原子边界。仅在 HTTP 头里附带一个编号,没有接收方验证,不能产生保护。某些存储会为资源保存最大已接受编号,但这也需要考虑编号归属、重建、权限与多资源操作。

即使下游比较编号,也不能笼统推导“旧调用一定不会发生”。在新编号尚未被它看到之前,旧编号是否还能被接受,取决于接收方的合同。真正要求实时执行许可的业务,可能还需要权威授权服务或受保护的短期凭据。

7.5 用暂停、到期与迟到提交验证

随书实验先使旧领取失效,经权威查询确认暂无效果并重排后,由新拥有者接手、调用工具取得回执,再尝试让旧拥有者携带同一业务回执写入成功。正确结果是旧写入被拒绝,工具账本没有重复效果,新的状态仍能被合理核查或完成。

另一个实验检查过期心跳。它必须返回资格失效,而不是悄悄延长时间。这类断言比只观察最后有一个成功状态更有解释力,因为最终成功也可能掩盖过两个拥有者都曾被接受。

测试中的时间注入和真实等待分别记录。部分边界测试直接将数据库期限设为过去,验证条件更新;进程崩溃实验实际等待 PostgreSQL 时钟超过租约,再进行核查。两者各有用途,不能把前者描述成已经测量真实故障恢复延迟。

练习。 worker A 的租约失效,worker B 已经领取任务,A 此时收到了早先工具调用的成功响应。请说明 A 能否更新本地终态、这份回执是否仍有业务价值,以及怎样交给当前核查者使用而不让 A 重新获得写入资格。