本地只完成一次,设备却执行了两次:故障模型与恢复边界¶
请求已发出,设备也完成了操作,但回复途中通道断开。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 之前的预算,还需给退避、设备服务与结果交付留空间。
多层各允许三次尝试,最坏可能形成相乘的发送次数。应统计一次业务意图触发了多少实际请求,而不只看 SDK 单次成功率。必要时为故障期额外工作设置独立额度,避免恢复流量挤掉正常控制工作。
退避可以减少同步重试尖峰,但不能修复错误的幂等契约,也不能保证设备会恢复。退避和最大次数是容量与业务决定的一部分。
恢复的进展指标应该来自业务¶
线程存活、循环执行、backend HELLO 和实际请求成功分别证明不同程度的进展。心跳处理正常时,某类请求仍可能因为队列、锁或设备能力长期停滞。
监测应结合状态停留时长、完成序号、入口 / 输出 / 数据水位、失败原因、old_generation 和恢复次数。watchdog 的喂养条件也应对应预期进展,而不是让一个与业务无关的定时线程持续喂狗。
如果 callback 在 owner 中无限阻塞,owner 无法自行诊断和结束自己的等待。需要外部观察者与隔离边界;用户态进程的分级终止方案不能直接照搬到内核或硬件活动。Linux 资源隔离说明这层责任。
把故障模型变成对照矩阵¶
| 注入点 | 核对本地 | 核对远端 / 后端 | 当前状态 |
|---|---|---|---|
| 重复 / 旧代际回复 | 不重复 terminal,不误完成新请求 | 消息身份 | 已有 core / runtime 回归 |
| 通道断连与恢复失败 | 请求终结,READY / FAILED 一致 | 后端创建与关闭 | 已有回归 |
| 副作用完成后丢回复 | 本地可报告结果未知 | operation 查询 / 去重 | 尚未实现业务设备协议 |
| 设备写记录中掉电 | 本地恢复流程 | 记录与真实状态一致 | 需要真实存储或可信模型 |
| 多层同时恢复 | 次数、时间、额外负载受限 | reset 影响范围 | 待设计与验证 |
已有证据见 验证范围。新增协议保证只有在实现、故障注入和目标验证都对应以后才进入支持范围。架构决定最终要回答:系统知道什么,不知道什么,谁来处置未知,以及恢复怎样避免扩大故障。