一个字段改了名,只有合计悄悄错了
目标
把合作方订单 API 的 1.3 和 1.4 并排摆放,逐字段分辨出什么会打破我们,然后制作只写我们所读取内容的消费者契约,以及每次都确认该契约的契约测试。还要接上同时接受旧版本和新版本的读取层。
为什么重要
连接中断的事故会触发告警,字段改名的事故不会。rec.get("region") 不会抛出异常,而是返回 None,增加的枚举值进不了我们任何一个分支,整数变成十进制字符串,合计就会悄悄地变掉。
所以集成的安全装置不是“认真读文档”,而是由机器每次都去比对的契约。契约中只写我们实际读取的字段,而不是合作方的完整 schema。全写进去的话,我们不用的字段一变更也会亮红灯,噪音一多,人就会把测试关掉。
版本升级也不会一次结束。旧版本和新版本并行几个月的期间,读取的一方必须同时接受二者,所以改了名字的位置用别名表、改了类型的位置用规范化函数,集中到一处。
评分器不会相信你写的句子。评分器会把你制作的合作方服务器启动在评分器所选的端口上并取得响应,再用评分器生成的输入重新运行你的判定器和比对器,核对答案。
步骤
- 创建 /root/contract/partner.py 并在端口 8011 上启动,将两个版本的响应分别保存到 /root/contract/v13.json 和 /root/contract/v14.json。
- 把两个响应的字段差异写入 /root/contract/diff.json,分为 added、removed、type_changed、enum_added 四栏。
- 创建 /root/contract/breaking.py,使它接收一条变更,并判定它是否会打破我们。
- 把只写我们所读取字段的消费者契约写入 /root/contract/order.contract.json。
- 创建 /root/contract/validate.py,使它比对契约与记录,并把违规分为 missing、type、enum 来记录。
- 创建 /root/contract/read_order.py,使它把 1.3 和 1.4 的响应都转换为同一种内部形态。
- 创建 /root/contract/contract_test.sh,使它把合作方当前的响应与契约比对,不一致时以非 0 的退出码结束。
- 在 /root/contract/contract_report.md 中分四节进行汇报。
参考
- 合作方服务器的运行契约:
python3 /root/contract/partner.py --port <포트> [--drift](占位符为端口)。/health返回{"ok": true, "versions": ["1.3", "1.4"]},/v1.3/orders和/v1.4/orders返回{"version": ..., "orders": [...]}。订单有 24 笔。 - 两个版本的差异如下。1.3 返回
order_idamount(整数)currencystatusregionupdated_at,1.4 返回order_idamount(十进制字符串)currencystatusmarketchannelupdated_at。1.4 的status中多了on_hold。 --drift是合作方在没有通知的情况下又一次变动的版本。第 7 步的契约测试必须以非 0 的退出码把它筛出来。- 判定器的运行契约:
python3 breaking.py --change <파일>(占位符为文件名)会返回{"breaking": true|false, "reason": "..."}。变更 JSON 是{"where": "response"|"request", "kind": "...", "field": "..."},kind 有 add_field · remove_field · rename_field · type_change · add_enum_value · field_becomes_optional · add_optional_field · add_required_field · relax_required 九种。同一个 kind 名称可能同时出现在响应一侧和请求一侧,这时答案是不同的。如果来了表中没有的 kind,不要回答“安全”,而要回答“会造成破坏”。 - 比对器的运行契约:
python3 validate.py --contract <파일> --records <파일>(占位符依次为契约文件与记录文件)会返回{"records": n, "ok": n, "violations": [{"index": i, "field": f, "kind": k}]}。kind 是 missing · type · enum。类型名称有 string · integer · decimal_string 三种。ok是没有任何违规的记录数。 - 读取层的运行契约:
python3 read_order.py --in <응답 파일>(占位符为响应文件)会返回规范化记录的列表。每条记录有order_idamount_krw(整数)currencystatusmarket五栏。 - 契约测试的运行契约:
bash contract_test.sh <BASE_URL>在没有违规时以 0 结束,有违规时以 1 结束。 - 本实验的判定规则:在响应中,只有增加字段是安全的,其余都视为破坏。在请求一侧,只有增加必填字段才会造成破坏。这是消费者视角的规则,并不是 RFC 规定的。
- 常见错误:把合作方的所有字段都写进契约(我们不使用的变更也会亮红灯);把金额规范化为 float(会产生舍入);把合作方服务器放在前台启动,导致终端被占住。
- 服务器要放在后台启动,等到
curl -sf http://127.0.0.1:8011/health能成功之后再继续。评分器不会查看你启动的进程,而是直接重新启动脚本。
启动同时提供两个版本的合作方
创建 /root/contract/partner.py 并在端口 8011 上启动,将 /v1.3/orders 和 /v1.4/orders 的响应分别保存到 /root/contract/v13.json 和 /root/contract/v14.json。两个版本的订单都是 24 笔。
用 flask 创建三条路径。/health 用来表示已准备就绪,两个版本的列表路径会以各自的字段名称和类型返回同样的 24 笔订单。用 argparse 接收 --port 和 --drift。如果在前台启动,终端会被占住,所以请放到后台启动,并等待 /health 返回 200。
以机器可读的形式提取两个版本的差异
在 /root/contract/diff.json 中写入 added removed type_changed enum_added 四栏。added 和 removed 是排好序的字段名称列表,type_changed 是 {"field": ..., "from": ..., "to": ...} 的列表(类型名称为 integer、string),enum_added 是 {"field": ..., "values": [...]} 的列表。被视为枚举的字段,限定为在新版本中不同取值不超过 6 个的字段。
只看两个响应文件的第一条记录,就能得到字段名称的集合,但枚举值必须全量扫描才能看到。类型名称有 integer 和 string 两种就够了。对于 status 这种只有几种取值的字段,请对两边的取值集合做减法。
分辨什么会打破我们
创建 /root/contract/breaking.py,使它对通过 --change <파일>(占位符为文件名)接收的一条变更进行判定,并返回 {"breaking": true|false, "reason": "..."}。响应一侧和请求一侧的规则不同。
响应是我们读取的一侧,所以只有增加是安全的。请求是我们发送的一侧,方向相反——可选字段增加,或必填变为可选,对我们来说什么事都没有。以 (where, kind) 这一对为键的一张表就足够了,遇到不认识的配对,不要回答“安全”,而要回答“会造成破坏”。
只写我们所读取内容的契约
在 /root/contract/order.contract.json 中写入消费者契约。version 为 1.4,unknown_fields 为 ignore,fields 中只写我们实际读取的五个字段(order_id、amount、currency、status、market),各自带上 type 和 required。currency、status、market 还要加上 enum。
你会想把合作方提供的字段全部写上,但如果写了我们不读取的字段(channel、updated_at),对方每改一次这些字段,我们的测试就会亮红灯。类型名称有 string、integer、decimal_string 三种,1.4 的金额是十进制字符串。status 的枚举值,请通过全量扫描 1.4 的响应来获得。
把契约与实际响应比对
创建 /root/contract/validate.py,使它接收 --contract 和 --records,并返回 {"records": n, "ok": n, "violations": [...]}。违规分为 missing type enum 三种,并在每条违规上加上 index 和 field。
一条记录可能有两个违规,所以 violations 中每条记录可能有多行。另一方面,ok 是没有任何违规的记录数,所以不能用总记录数减去违规行数的方式得到。非必填字段缺失不算违规,对类型本身已经错误的值,不要再去检查枚举。
同时接受旧版本和新版本
创建 /root/contract/read_order.py,使它通过 --in <응답 파일>(占位符为响应文件)同时读取 1.3 和 1.4 的响应,并转换为同一种内部形态。每条记录有 order_id amount_krw(整数)currency status market 五栏,未知字段丢弃。
改了名字的位置用一张别名表,改了类型的位置用一个规范化函数来集中处理。如果到处散布 if,要停掉旧版本时就不知道该删哪里。金额不要经过 float——对 "12300.00" 以小数点为界切开并转换为整数,就不会产生舍入。
抓出没有通知就又变动的合作方
创建 /root/contract/contract_test.sh,使它通过 bash contract_test.sh <BASE_URL> 把合作方当前的 /v1.4/orders 与契约比对。没有违规时以 0 结束,有违规时以 1 结束,而且有违规时,屏幕上要留下有多少条。
直接使用前面制作的 validate.py 和 order.contract.json。要新写的只有取得响应的部分和退出码。请确认:对以 --drift 启动的合作方运行时得到 1,对平常的合作方运行时得到 0,两种情况都要确认。
版本升级检查报告
在 /root/contract/contract_report.md 中分为 ## 무엇이 바뀌었나 ## 무엇이 우리를 깨뜨리나 ## 우리가 지킬 계약 ## 다음부터 어떻게 잡나 四节来写(韩文,依次意为“改变了什么”“什么会打破我们”“我们要守住的契约”“今后如何抓住”)。前面步骤中得到的字段名称和违规数量必须写进正文。
读者也许不是我们的团队负责人,而是合作方的负责人。不要写“坏了”,而要写“哪个字段如何变化,导致我们这边的什么出了偏差”。四节的标题保持不变,数字用你得到的值来填写。