删除幂等键也意味着保证结束
目标
区分已完成和进行中的记录,并实现保留期限与分批清理。
为什么重要
表变得很大之后,把旧的幂等键全删掉了。结果仍在支付中的请求的记录也消失了,重试就被当作新请求进来。已完成响应的保留期限,与进行中任务的所有权,并不适用同一种过期策略。清理不是简单的 DELETE,而是一次改变保证范围的状态转换。
步骤
- 在
/root/work/idem-retention-lab/service.py中,init_db(path) 以幂等方式创建 keys(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,status TEXT NOT NULL,response TEXT,expires REAL NOT NULL)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-retention-lab
test -e /root/work/idem-retention-lab/service.py || cp /opt/fixtures/ten_labs/idem-retention-lab/service.py /root/work/idem-retention-lab/service.py
cd /root/work/idem-retention-lab
-
在
/root/work/idem-retention-lab/service.py中,reserve(path,key,digest,expires) 把不存在的键以 pending、response=NULL 插入并返回 True。对已有的键,无论是否过期都不改动,返回 False。 -
在
/root/work/idem-retention-lab/service.py中,finish(path,key,digest,response) 只对 key 和 fingerprint 都一致且为 pending 的行,把 response 以 JSON 保存并改为 done,返回 True,其余返回 False。 -
在
/root/work/idem-retention-lab/service.py中,fetch(path,key,now) 对 done 且 expires>now 的行,把 response 按 JSON 解析后返回,其他情况返回 None。 -
在
/root/work/idem-retention-lab/service.py中,expired(path,now,limit=10) 检查 limit 是 1–100 的 int(不含 bool)。它按 id 升序,最多返回 limit 个 done 且 expires<=now 的 id。 -
在
/root/work/idem-retention-lab/service.py中,purge(path,now,limit=10) 用与 expired 相同的 limit 校验、条件和排序挑出候选,在同一个事务中删除,并返回被删除的 id 列表。不删除 pending。 -
在
/root/work/idem-retention-lab/service.py中,counts(path) 返回 {pending:数量, done:数量}。即使没有某个状态,键和 0 也必须存在。 -
在
/root/work/idem-retention-lab/service.py中,retention_cycle(path,key) 以 expires=10、digest='v1' 预留,并用 {receipt:1} 完成。在 now=10 时执行 purge,然后用 digest='v2'、expires=20 对同一个键重新预留,并返回该结果的 bool。该函数以空 DB 为前提。
参考
- 无需联网和安装软件包,在现有的 lab-dev 环境中进行。
- 每个步骤都在 45 秒的评分预算内运行。不要加入真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 检查。不要直接返回预期值的常量,而要实现契约。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:它并不保证无限期的 exactly-once 效果。外部系统可能重新发送很久以前的请求,因此保留期限要与提供方的重试策略相匹配。如何恢复被遗弃的 pending 的租约与补偿策略,是另外的实验要负责的内容,这个清理函数不会凭猜测去删除它们。
把是否完成与过期分开
在 /root/work/idem-retention-lab/service.py 中,init_db(path) 以幂等方式创建 keys(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,status TEXT NOT NULL,response TEXT,expires REAL NOT NULL)。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-retention-lab
test -e /root/work/idem-retention-lab/service.py || cp /opt/fixtures/ten_labs/idem-retention-lab/service.py /root/work/idem-retention-lab/service.py
cd /root/work/idem-retention-lab
不能只看过期时间就断定业务已经结束。
保存后用 bash /opt/lab/checks/idem-retention-lab/01-contract.sh 确认。
预留进行中的键
在 /root/work/idem-retention-lab/service.py 中,reserve(path,key,digest,expires) 把不存在的键以 pending、response=NULL 插入并返回 True。对已有的键,无论是否过期都不改动,返回 False。
删除记录与复用键要分开,并明确地加以控制。
保存后用 bash /opt/lab/checks/idem-retention-lab/02-contract.sh 确认。
只完成当前请求
在 /root/work/idem-retention-lab/service.py 中,finish(path,key,digest,response) 只对 key 和 fingerprint 都一致且为 pending 的行,把 response 以 JSON 保存并改为 done,返回 True,其余返回 False。
连指纹也一起比较,防止正文不同的 worker 覆盖已完成的响应。
保存后用 bash /opt/lab/checks/idem-retention-lab/03-contract.sh 确认。
只重放有效的已完成响应
在 /root/work/idem-retention-lab/service.py 中,fetch(path,key,now) 对 done 且 expires>now 的行,把 response 按 JSON 解析后返回,其他情况返回 None。
固定下这份契约:在 now==expires 的边界上不再重放。
保存后用 bash /opt/lab/checks/idem-retention-lab/04-contract.sh 确认。
限制清理候选
在 /root/work/idem-retention-lab/service.py 中,expired(path,now,limit=10) 检查 limit 是 1–100 的 int(不含 bool)。它按 id 升序,最多返回 limit 个 done 且 expires<=now 的 id。
一次性清空整张表会拉长锁的持有时间,也容易把进行中的行混进来。
保存后用 bash /opt/lab/checks/idem-retention-lab/05-contract.sh 确认。
把选择和删除放在同一个事务里
在 /root/work/idem-retention-lab/service.py 中,purge(path,now,limit=10) 用与 expired 相同的 limit 校验、条件和排序挑出候选,在同一个事务中删除,并返回被删除的 id 列表。不删除 pending。
不要在查询候选和删除之间,留下状态可能发生变化的缝隙。
保存后用 bash /opt/lab/checks/idem-retention-lab/06-contract.sh 确认。
实际统计各状态的数量
在 /root/work/idem-retention-lab/service.py 中,counts(path) 返回 {pending:数量, done:数量}。即使没有某个状态,键和 0 也必须存在。
必须能够观测到,清理之后未完成的行有没有消失。
保存后用 bash /opt/lab/checks/idem-retention-lab/07-contract.sh 确认。
确认删除之后保证范围就结束了
在 /root/work/idem-retention-lab/service.py 中,retention_cycle(path,key) 以 expires=10、digest='v1' 预留,并用 {receipt:1} 完成。在 now=10 时执行 purge,然后用 digest='v2'、expires=20 对同一个键重新预留,并返回该结果的 bool。该函数以空 DB 为前提。
不掩盖“清理之后键会变成新请求”这个局限,而是亲自重现它。
保存后用 bash /opt/lab/checks/idem-retention-lab/08-contract.sh 确认。