跳转至

本地只完成一次,设备却执行了两次:故障模型与恢复边界

请求已发出,设备也完成了操作,但回复途中通道断开。SDK 将本地请求标为失败,业务重试,设备于是又执行了一次。一次 terminal 通知的本地守恒,并不自动提供远端副作用恰好一次。

前置阅读:SDK 契约、代际恢复案例。公开代码提供身份、终态和显式恢复;本章讨论它们在什么故障模型下成立,以及面向真实设备还需补哪些协议责任。

先把故障位置和可观察结果分开

故障位置 本地可能看见 远端结果可能是什么
API 尚未接受 同步拒绝 / admission 拒绝 没有因这次调用开始的新工作
已 ACCEPT,尚未发送 本地超时或停止失败终态 未执行,但应核对发送边界
发送后未收到回复 超时、断连或取消 未执行 / 已执行 / 正在执行
回复收到,通知尚未交付 callback 阻塞、进程退出 操作已执行,业务可能尚不知道
恢复后收到旧回复 old_generation 等计数 旧操作可能已结束或仍有副作用

“没有收到成功”描述本地知识,不等于“设备没有执行”。对调节寄存器、写 Flash、启动电机等不同操作,重试风险也不同。

故障模型应说明允许出现的崩溃、消息丢失、重复、延迟、乱序、reset 和存储丢失。没有覆盖的故障单列,不将一个容器回归扩大为所有环境都成立的保证。

三层身份承担不同责任

公开 SDK 的 key 包含 instance、generation 和 id:instance 区分实例,generation 隔离会话,id 区分同一代际的请求。匹配这份完整身份,可以拒绝旧回复误完成新请求。

这仍不等于业务幂等键。SDK 重建以后可能产生新的 generation 和 id;业务若把相同操作作为新请求提交,设备并不知道这是同一个意图。

可以增加一个由业务定义的 operation key,重试同一意图时保持它稳定。协议必须进一步规定:

  • 哪些操作支持去重,哪些只能查询结果。
  • 去重记录保存多久,是否跨设备 reset 和掉电。
  • 去重记录与真实副作用是否原子关联。
  • key 冲突、容量满、记录失效时怎样返回。

这些机制尚未在公开模拟器实现,不能将 generation 检查当作已经具备的设备去重。

去重记录与副作用之间仍可能崩溃

假设设备先执行副作用,再写完成记录:两步之间掉电,重试时没有记录,会再执行。若先写“完成”再执行,掉电以后可能返回成功却没有完成副作用。

执行 → 记录:存在已执行但未记录的窗口
记录 → 执行:存在已记录但未执行的窗口

需要结合设备事务能力选择方案:让状态变化与记录一起提交;使用可重复的目标状态操作;提供 operation 状态查询和对账;或明确告诉调用者结果未知,由业务处置。

例如“设置目标值为 7”往往比“当前值加 1”更容易设计成可重复操作,但设备状态、并发写者与物理过程仍要纳入契约。不能只换消息名字就宣称幂等。

取消也要区分本地和远端

当前能力查询报告 remote_cancel 不支持。本地 cancel 可以终结等待和通知义务,不能据此保证设备已撤回操作。

支持远端取消时还需定义:取消到达前是否已开始,取消与完成谁赢,取消成功究竟表示“不再执行”还是“已请求停止”,以及设备最后访问 buffer 的时点。

接口可以区分确定未执行、确定执行、结果未知。是否增加状态或查询 API,是行为契约的变化,需同步能力、错误码、统计和调用者处理,而不只是追加一个 cancel 命令。

谁拥有恢复决定

请求重试、通道重建、设备 reset 和整机重启有不同的影响范围:

层级 可能恢复什么 可能损失什么
单请求 暂时丢包或短等待 重复副作用、额外负载
通道 IPC / 会话故障 该会话所有 live 请求
设备 固件或设备状态停滞 多客户端状态、在途硬件操作
系统 更大范围失去进展 未持久化状态与其它服务

由一个明确的管理层选择升级条件,避免业务、SDK、driver 和设备分别无限恢复。对每级写清检测、最多尝试次数、等待方式、失败终态和需要保存的证据。

公开 core 由外部显式发起 recover,重建失败进入 FAILED,没有自动无限恢复。要扩展自动恢复,应先建立预算与负责人,而不是在每个错误分支加入循环。

重试预算要扣实际剩余时间

以设计示例说明:业务总预算 100 ms,第一轮已经等待 70 ms。第二轮不能又拿到完整 100 ms;它只剩总 deadline 之前的预算,还需给退避、设备服务与结果交付留空间。

业务最终 deadline 固定
每次尝试预算 ≤ 最终 deadline − 当前时间 − 后续交付余量
重试总数与额外工作量有上限

多层各允许三次尝试,最坏可能形成相乘的发送次数。应统计一次业务意图触发了多少实际请求,而不只看 SDK 单次成功率。必要时为故障期额外工作设置独立额度,避免恢复流量挤掉正常控制工作。

退避可以减少同步重试尖峰,但不能修复错误的幂等契约,也不能保证设备会恢复。退避和最大次数是容量与业务决定的一部分。

恢复的进展指标应该来自业务

线程存活、循环执行、backend HELLO 和实际请求成功分别证明不同程度的进展。心跳处理正常时,某类请求仍可能因为队列、锁或设备能力长期停滞。

监测应结合状态停留时长、完成序号、入口 / 输出 / 数据水位、失败原因、old_generation 和恢复次数。watchdog 的喂养条件也应对应预期进展,而不是让一个与业务无关的定时线程持续喂狗。

如果 callback 在 owner 中无限阻塞,owner 无法自行诊断和结束自己的等待。需要外部观察者与隔离边界;用户态进程的分级终止方案不能直接照搬到内核或硬件活动。Linux 资源隔离说明这层责任。

把故障模型变成对照矩阵

注入点 核对本地 核对远端 / 后端 当前状态
重复 / 旧代际回复 不重复 terminal,不误完成新请求 消息身份 已有 core / runtime 回归
通道断连与恢复失败 请求终结,READY / FAILED 一致 后端创建与关闭 已有回归
副作用完成后丢回复 本地可报告结果未知 operation 查询 / 去重 尚未实现业务设备协议
设备写记录中掉电 本地恢复流程 记录与真实状态一致 需要真实存储或可信模型
多层同时恢复 次数、时间、额外负载受限 reset 影响范围 待设计与验证

已有证据见 验证范围。新增协议保证只有在实现、故障注入和目标验证都对应以后才进入支持范围。架构决定最终要回答:系统知道什么,不知道什么,谁来处置未知,以及恢复怎样避免扩大故障。