TT Lab
开始
学习 学习路径 课程

ACK前停机的零食售货机

恢复在ACK之后终止的发送进程

在 TT Lab 中继续学习

一句话总结

未完成的条目只有在收到可核实的 ACK 之后才标记为完成;一旦失败,就保留已经完成的条目,用相同的业务 ID 重新发送。

为什么需要它

即使分别把受理 DB 和仓库 DB 都做得很安全,投递循环中的一行代码也可能毁掉恢复。如果在发送请求之前就保存 sent=1,在投递过程中死掉的条目就会在下一次查询时消失。如果在发送之后再标记,则在仓库反映与标记之间发生终止时,就会产生重发。无论哪种做法,都不能让发送恰好只发生一次。本实验选择允许重发,并保护接收效果。

运维人员应当问的不是“这个请求成功了吗?”,而是“证据到哪个状态为止?”。orders 中有、outbox 中以未完成状态存在、仓库 inbox 中有、ACK 回到了发布方、sent 已保存,是各不相同的观测。前一个阶段并不会自动证明后一个阶段。在故障应对中把这种差别整理成表,可以减少为了消除不确定性而删除数据的失误。

工作原理

按顺序处理小批次

pending 会按 outbox 的插入顺序 seq 读取 sent=0 的条目。不按 ID 的字典序或以秒为单位的时刻排序。ID z 可能比 a 更早被受理,同一时刻也可能有多个条目。每次只返回 1–16 个,查询本身不会改变状态。如果一查询就改为完成,就会丢失尚未发送的条目。

dispatch 会对所选批次中的每个条目调用 send。收到严格的 True ACK 之后,调用 mark_sent。在第一次失败时停止,并传递原始错误。之前已经完成的条目保持完成,失败的条目和尚未尝试的后续条目则保持未完成。重新执行时,会从那个位置接着往下走。本实验处理的是单个投递循环的顺序,并不实现多个工作者同时领取同一批次所需的 claim、lease 和 fencing。

例如,在 A、B、C 三项中,A 在收到 ACK 之后保存了完成标记,而 B 的响应丢失了,那么下一个批次中会剩下 B 和 C。即使 B 已经在仓库中应用,由于是用相同的 ID 重发,inbox 会阻止重复效果。C 则还没有发送。跳过 B 的失败、从 C 开始完成的策略在某些系统中也是可行的,但对于顺序重要的业务,含义就不同了。本实验的 stop-on-first-failure 是明确选定的契约。

防止不必要的锁和参数被修改

在发送之前就完成 pending 查询,并关闭 SQLite 写事务。这样在 send 很慢或收不到响应期间,不会连新订单的受理也锁住。测试中会确认,在 send 回调内,单独的 DB 连接能否启动 BEGIN IMMEDIATE。这比仅仅读取 con.in_transaction 的值更进一步,查看的是真正的竞争连接能否进入写边界。

还要考虑到,传给回调的 dict 可能被对方修改。如果所选条目是 B,而回调把参数中的 id 改成了 C,再把这个被改过的值直接用于完成标记,就可能丢掉 C。教学用的 event 只有字符串和整数,所以 dict 的副本就足够了。用原来选定的值留下标记,用副本发送。如果是包含嵌套对象的其他契约,仅靠浅拷贝可能起不到保护作用。

强制制造真实的故障顺序

最终检查会向临时 source DB 中放入 snack-7,并在单独的进程中执行 dispatch。HTTP 服务器会把接收 ID 和库存提交到单独的 sink DB。发布方在确认成功 ACK 之后,立刻在 after-delivery 钩子中以 os._exit(73) 终止。finally 和正常的 close 都不会被调用。会通过退出码确认第一个进程确实是在那个位置终止的。

父检查器会用独立连接读取:source 中是否留有未完成的条目,sink 中是否留有库存 7。接着启动新的发布进程。服务器观测到的请求必须是两个相同 ID 的请求,最终库存必须是 7。在第二个进程中,outbox 必须变为完成。这不是只重新创建 Python 对象的状态,而是让不同进程重新打开同一个文件的测试。

在另一种情况下,服务器在接收提交之后不发送 HTTP 响应就关闭连接。此时发布方会收到错误,必须把该条目保留为未完成。重新连接并发送同一个条目时,服务器会把它当作有效重复并发送 ACK,库存保持不变。ID 错误的 ACK、过大的响应和重定向,也都不会被当作完成的证据。send_http 只允许本实验的 loopback /events,不使用代理环境变量,也不跟随重定向。

在现场相遇的样子

在真实运维中,不会仅凭发送成功的次数来判断状态。要分别查看未完成条目数、最久未完成条目的存续时长、尝试次数、接收重复数和冲突数。如果前面的某一个条目存在永久性的格式错误,后面的条目就可能一直被阻塞。应当准备告警、隔离、修正和重新驱动的流程,而不是无限重试。被隔离并不等于成功,改变顺序所带来的影响也要一并记录。

重试等待需要上限和抖动,但这次的 dispatch 只执行一次调用的有限批次。不会声称实现了自动重试调度器或高可用的任务分发。source 和 sink 文件也会在 LabHub 会话结束时消失。这里验证的是进程终止后重新使用同一块磁盘的情形。主机断电、磁盘故障、备份恢复以及在互联网上的性能,需要另行验证。

下一项实验要做什么

在 worker.py 中依次实现:事件验证、存储初始化、原子受理、未完成查询、重复接收处理、完成标记、有限批次投递、HTTP ACK 确认。提供的检查器会自行创建临时 DB 和 loopback 服务器,并运行正确答案的代码。空的函数框架只是语法有效,并不能代替答案。实验之后的测验会整理“投递两次、应用一次”这一结果所能证明的范围,以及剩余的运维责任。

通过官方文档进一步阅读