不要用昨天的游标读取新学期库存
一句话总结
编号相同,也可能是不同代次的事件。恢复就是安装所允许代次的快照,再从其下一个编号开始有限地追赶的过程。
为什么需要它
学校节日第一天的库存记录一直到了 800 号。第二天重新开设了仓库,日志编号从 0 重新开始。如果只发送电子屏上留下的 800 这个数字,服务器可能会误以为连将要发生的事件都已经处理过了。或者,副本可能会把新日志当作过去的重复而丢弃。这是因为没有明确指出编号唯一的范围而产生的问题。
这里把 epoch 用作 day-A、day-B 这样的代次标识符。在一个代次内部不会复用编号,只有明确开始新的代次时,才会赋予不同的 epoch。实验中的字符串只是便于说明的示例,并不意味着只要日期相同就是安全的。真实的系统需要能够区分恢复、重新生成和生产环境的标识策略。如果让 epoch 由任意用户输入来选择,还可能造成读取其他客户状态的安全问题。
工作原理
install_snapshot 除了 snapshot 之外,还接收 expected_epoch。只有 snapshot.epoch 与 expected_epoch 相同,才能安装。expected_epoch 是由已经信任的连接配置传入的值,如果直接从要检查的 JSON 中取出,检查就成了循环论证。格式正确的字符串,也不能证明有权读取那个仓库。认证、权限和传输保护,需要由真实的服务另行保证。
同一代次中更旧的快照以 Conflict 拒绝。如果 last 相同而 total 不同,同样是冲突。如果代次、last、total 都相同,就无需重新安装,返回 False,也不会删除仍然保留的单个事件。如果是所允许的其他代次的快照,即使编号更小也可以替换。不能仅凭 day-B 的 0 比 day-A 的 800 小这种数字比较,就拒绝新的代次。
安装快照之后,此前的单个事件就不再保留在副本中了。例如 total=13、last=5 的快照,只能告诉你 0 到 5 的总和,却无法告诉你 4 号的 delta 是什么。之后如果 (4,2) 才到来,由于无法证明它是内容相同的重复,就把它归类为 CoveredBySnapshot。如果悄悄忽略所有过去的编号,就可能掩盖内容冲突。反过来,如果保留下来的单个事件的编号和 delta 都相同,那就是可以验证的重复,不再重复累加效果。
新的批次必须是连续编号。如果最后应用的是 5,那么第一个新编号就是 6。如果收到只有 6 和 8 的批次,并把游标提升到 8,7 就消失了。另外,如果提交了 6 的效果之后在 8 处报错,那么当调用方认为整个批次失败而重试时就会出现混乱。本实验把最多 16 个的整个批次放在一个事务中应用,并先检查格式和内部顺序。把处理记录、库存和最后编号绑在同一个边界内。
在现场相遇的样子
sync_once 是一次同步尝试。它会确认服务器的代次是否符合预期,并用副本的游标请求重放。如果超出范围或代次不同,就获取快照并安装,然后只读取大于 snapshot.last 的事件。它返回应用了最多 16 个事件之后的状态,并不承诺已经完全追上服务器的最新状态。调用方要比较结果游标与观测时点的服务器游标,并决定是继续追赶,还是向用户显示延迟。
尤其是在安装快照期间,原始日志可能再次被截断。起初快照包含 last=20,如果安装之后原始日志写入了 21 号并连它也删除了,就无法重放 21。这种情况下,这次 sync_once 会再次传递 ResyncRequired。它不会删除已经安装的 last=20 的有效状态,而是在下一次有限调用中获取更新的快照。如果掩盖失败,或在 catch 内无休止地重试,就会因追不上原始日志的快速保留截断而白白消耗资源。
sync_files 负责打开和关闭两个不同本地文件的边界。即使路径字符串不同,也可能通过硬链接指向同一个文件,所以还要确认实际的文件身份。如果原始文件不存在,不会创建空 DB 并假装恢复成功。即使打开副本失败,也会关闭已经打开的原始连接。文件路径的前提是学习者可控的一次性文件夹,并没有实现对攻击者同时更改链接的生产服务器所需的路径竞争防御。
下一项实验要做什么
在 8 个步骤的综合实验中,要完成 apply_batch、sync_once 和 sync_files。最后会让真实的子进程以退出码 73 结束,并用独立连接重新打开 DB。在提交前后,都要比较游标、库存和单个事件是整个保持旧状态,还是整个变为新状态。会验证同一个文件的重启,但不验证磁盘丢失、服务器之间的共识、自动故障切换、TLS 和用户权限。即使扩展为下一个任务,也需要做把已验证的边界逐步扩大的工作。
参考:Python sqlite3 连接与事务、Python os.path.samefile。epoch、冲突分类和有限恢复循环,是这个学习协议的契约。