相同的键不一定代表相同的请求
目标
把键的范围、请求体规范化和键复用冲突分开,来判定一次请求是否为重新发送。
为什么重要
两个客户碰巧使用了同一个幂等键,结果一个客户收到了另一个客户的响应。在另一起请求中,同一个键对应的金额已经变了,系统却仍返回之前的成功结果。只比较幂等键本身,就会忽略请求的含义和安全边界。键要绑定到租户和操作范围,请求体的含义则要用单独的指纹来检查。
步骤
- 在
/root/work/idem-fingerprint-lab/service.py中,valid_key(value) 只会原样返回由英文字母、数字、下划线、连字符组成的 1–64 个字符,其他输入一律抛出 ValueError。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab
-
在
/root/work/idem-fingerprint-lab/service.py中,canonical(body) 只接受 dict,并返回以 sort_keys=True、separators=(',',':')、ensure_ascii=False、allow_nan=False 序列化得到的 JSON 字符串。无法序列化的值统一抛出 ValueError。 -
在
/root/work/idem-fingerprint-lab/service.py中,fingerprint(body) 是对 canonical(body) 的 UTF-8 字节应用 SHA-256 后得到的 64 位 hex 字符串。 -
在
/root/work/idem-fingerprint-lab/service.py中,scoped_key(tenant, method, path, key) 用 valid_key 校验 tenant 和 key,并把 method 转为大写。path 必须是以 / 开头的字符串。把这四个值编码成 separators=(',',':') 的 JSON 数组并返回。 -
在
/root/work/idem-fingerprint-lab/service.py中,classify(record, digest) 在 record=None 时返回 'new',在 record['fingerprint']==digest 时返回 'replay',否则返回 'conflict'。 -
在
/root/work/idem-fingerprint-lab/service.py中,remember(records, key, digest, response) 把 {fingerprint:digest, response:response 的 deepcopy} 保存到新键下。如果键已存在,就抛出 ValueError,并保留原有记录。 -
在
/root/work/idem-fingerprint-lab/service.py中,replay(record) 返回 record['response'] 的深拷贝副本。 -
在
/root/work/idem-fingerprint-lab/service.py中,execute(records, tenant, method, path, key, body, action) 会计算 scope 和指纹。结果为 new 时,把 action() 的结果 remember 下来并返回副本;结果为 replay 时,返回已有响应的副本;结果为 conflict 时,抛出 ValueError。action 抛出的异常要向外传播,并且不留下记录。
参考
- 无需联网和安装软件包,在现有的 lab-dev 环境中进行。
- 每个步骤都在 45 秒的评分预算内运行。不要加入真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 检查。不要直接返回预期值的常量,而要实现契约。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:这个实验用单进程的内存字典,来学习把键与请求含义分开的契约。进程故障之后的数据保留,以及多个 worker 的并发,留到下一个 SQLite 实验再讲。哈希不是加密,也不声称 JSON 规范化已经统一了所有语言的数字表示,成了国际标准。
校验键的语法
在 /root/work/idem-fingerprint-lab/service.py 中,valid_key(value) 只会原样返回由英文字母、数字、下划线、连字符组成的 1–64 个字符,其他输入一律抛出 ValueError。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab
限制键的长度和允许的字符,不把空键当作正常的重新发送来处理。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/01-contract.sh 确认。
对象顺序折叠,数组顺序保留
在 /root/work/idem-fingerprint-lab/service.py 中,canonical(body) 只接受 dict,并返回以 sort_keys=True、separators=(',',':')、ensure_ascii=False、allow_nan=False 序列化得到的 JSON 字符串。无法序列化的值统一抛出 ValueError。
如果对数组排序,就会改变用户所请求的操作顺序。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/02-contract.sh 确认。
计算请求体指纹
在 /root/work/idem-fingerprint-lab/service.py 中,fingerprint(body) 是对 canonical(body) 的 UTF-8 字节应用 SHA-256 后得到的 64 位 hex 字符串。
Python 的 hash() 每个进程都会变化,因此不能用作存储的指纹。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/03-contract.sh 确认。
按租户和操作区分键
在 /root/work/idem-fingerprint-lab/service.py 中,scoped_key(tenant, method, path, key) 用 valid_key 校验 tenant 和 key,并把 method 转为大写。path 必须是以 / 开头的字符串。把这四个值编码成 separators=(',',':') 的 JSON 数组并返回。
与简单地用分隔符拼接相比,对结构进行编码能让边界更清晰。路径的大小写要保留。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/04-contract.sh 确认。
区分三种判定
在 /root/work/idem-fingerprint-lab/service.py 中,classify(record, digest) 在 record=None 时返回 'new',在 record['fingerprint']==digest 时返回 'replay',否则返回 'conflict'。
不能仅仅因为键已经存在,就把所有重复请求都当作成功来重放。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/05-contract.sh 确认。
保存响应时做拷贝
在 /root/work/idem-fingerprint-lab/service.py 中,remember(records, key, digest, response) 把 {fingerprint:digest, response:response 的 deepcopy} 保存到新键下。如果键已存在,就抛出 ValueError,并保留原有记录。
响应里的列表如果也不拷贝,嵌套的状态就会被共享。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/06-contract.sh 确认。
读取响应时同样做拷贝
在 /root/work/idem-fingerprint-lab/service.py 中,replay(record) 返回 record['response'] 的深拷贝副本。
防止修改了第一次响应的调用方,连带改变后续重新发送的结果。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/07-contract.sh 确认。
业务函数只调用一次
在 /root/work/idem-fingerprint-lab/service.py 中,execute(records, tenant, method, path, key, body, action) 会计算 scope 和指纹。结果为 new 时,把 action() 的结果 remember 下来并返回副本;结果为 replay 时,返回已有响应的副本;结果为 conflict 时,抛出 ValueError。action 抛出的异常要向外传播,并且不留下记录。
只有连业务函数的调用次数和失败之后残留的记录都检查了,才能看清重新发送的契约。
保存后用 bash /opt/lab/checks/idem-fingerprint-lab/08-contract.sh 确认。