心跳超时后把任务交给新 Worker原 Worker 不一定真的停止了。它可能经历了一次长暂停恢复以后继续把旧结果写进数据库。只记录“现在是谁持有租约”还不够保护外部副作用。LangGraph 的持久化说明把检查点与跨线程存储分开它也明确指出内存检查点不会跨进程重启保留。官方文档 这给故障接管划了一条很实用的线恢复上下文、分配所有权、阻止陈旧写入需要分别设计。接管后存储端也必须认识新一任 Workerfencing token 是单调递增的所有权代次。新 Worker 得到更大的代次资源端拒绝旧代次的写入。它不是由旧 Worker 自觉检查的一面小旗而是最终写入位置必须执行的约束。Kleppmann 在讨论分布式租约时用这个机制解释暂停与延迟请求带来的风险。原文即使代次检查正确同一代次内的重试也可能重复提交因此还需要操作幂等键。检查点中的“下一步”只是一条执行意图如果外部系统已经产生副作用却没回 ACK重新跑一遍仍可能重复扣款或重复创建资源。让两个 Worker 在同一段程序里相遇下面的资源端既记最高代次也记已经提交的操作 ID。先让代次 8 写入再提交代次 7 的迟到请求陈旧请求必须失败同一操作的重试则返回原结果。图中的 7 与 8 是所有权代次不是时间戳也不是 Worker 的业务优先级。检查点回放把上下文送到新 Worker资源端的 fencing 检查负责另一条防线。class FencedResource: def __init__(self): self.highest_epoch 0 self.values {} self.operations {} def write(self, epoch, operation_id, key, value): if epoch self.highest_epoch: raise ValueError(stale owner) self.highest_epoch epoch if operation_id in self.operations: previous self.operations[operation_id] if previous ! (key, value): raise ValueError(idempotency key reused with different input) return already_committed self.values[key] value self.operations[operation_id] (key, value) return committed resource FencedResource() assert resource.write(8, op-B, answer, new) committed assert resource.write(8, op-B, answer, new) already_committed try: resource.write(7, op-A, answer, late) except ValueError: pass else: raise AssertionError(stale worker overwrote new owner) assert resource.values {answer: new} print(epoch 7 rejected; epoch 8 retry committed only once)例子跑通以后还要验证什么这段代码已在本地 macOS、CPython 3.9.6 下运行实际输出为epoch 7 rejected; epoch 8 retry committed only once。它验证的是例子中的数据约束与分支不包含线上服务的吞吐或延迟测量。这个资源端是单进程教学对象没有实现分布式一致性或崩溃持久化。真实数据库必须把代次比较、幂等检查和数据写入放在同一原子提交边界不支持 fencing 的外部服务需要另外设计幂等或补偿。未运行 LangGraph、数据库接管或故障注入测试。延伸阅读LangGraph Persistence检查点、持久化边界与内存存储的限制。How to do distributed locking2016 年对暂停、租约与 fencing 的原始工程论证。