L05 参考解:将对话原型服务化:任务、审批、事件流
这是可讨论的参考设计,不是唯一架构。先完成实验题面,再对照本解。
关键推导
创建Run与“以后记得发消息”必须原子绑定。Outbox并不把网络投递变成exactly-once;它把“可能丢掉要做的事”转换为“允许重复传递同一件事”。重复消费再由请求/运行幂等性控制。
审批必须被表示为一个持久对象,不是循环内input()。提案内容改变后需要重新批准;过期边界是now>=expires_at无效。审批已消费后的重复确认不应产生新操作。
参考RunLedger刻意只实现创建、抢占、受fence保护的worker状态更新、事件读取和Outbox记录。完整API、等待恢复、取消与reconciler是本实验需要补齐的项目层;不能把这个内核伪装成完整工作流引擎。
参考算法与实现位置
BEGIN
INSERT run UNIQUE(tenant,user,request_key)
INSERT event(run,seq=1,type=created)
INSERT outbox(run,kind=dispatch)
COMMIT
publisher: read pending → publish same message_id → mark sent
consumer: deduplicate message/run → acquire execution ownership → run
SSE每个事件使用稳定id;页面刷新后先读取快照,再衔接游标事件,处理快照与流之间的版本边界。
应当得到的可检查结果
| 故障/动作 | 期望 |
|---|---|
| 同key同请求POST两次 | 同run_id,仅一条初始Outbox |
| 同key不同请求 | 409语义冲突 |
| 事务提交后未投递进程退出 | Outbox仍可恢复投递 |
| 投递成功但未mark_sent | 允许重复投递,消费者必须去重 |
| after_seq=1重连 | 只回seq>1;客户端仍处理重复网络帧 |
| 等待审批时claim | NOT_RUNNABLE |
| 其他租户读run/events | NOT_FOUND |
怎样判断替代实现是否等价
替代实现必须保留题面中的身份来源、状态归属和失败语义。数据库/框架可以更换,但应把相同故障窗口重新跑一遍。只在正常路径输出一致,不足以证明恢复与授权等价。异步或并发实现还必须解释取消传播、竞争窗口和资源回收。
常见错误与修正
第一类错误是把协议或框架提供的局部保证放大成业务保证:格式正确不等于授权,执行结束不等于业务成功,持久化不等于外部副作用恰好一次。修正方式是找到事实源并把不变量放在拥有该事实的层。
第二类错误是用演示数据替代证据。已有fixture可以检查确定性公式与状态机,但真实模型、网络或容器的结论需要实际运行。报告同时写出“已验证”“仅模拟”“未验证”,比宣称全部完成更有工程价值。
实现范围与限制
没有交付FastAPI服务、认证系统、消息Broker或浏览器SSE实现;它们的字段、错误语义与验收条件已完整规定,需在个人项目/网页开发中实现。教学网站本身不必部署这些危险服务,可用纯状态机展示。
开放题评分要点
事务/幂等25;审批25;事件与重连20;权限15;服务边界与测试15。 正确的反例、失败分析和替代方案论证应获得分数;框架堆叠、只报最终成功截图、删除失败成本不应获得同等评价。