Webhook 重试测试实战:用 Idempotency-Key 故障序列确定性复现断连
原文:https://dev.to/emma_teelylabs/testing-webhook-retries-deterministically-a-fault-sequence-per-idempotency-key-1k19(作者 @emma_teelylabs)
「10% 的错误率只能证明发送方会重试,却证明不了重试是正确的,而且我没法把 10% 这种参数放进 CI。」这条针对随机故障注入的批评切中要害。真正要抓的 bug 是一个非常具体的场景:
- 我的服务发出 webhook。
- 接收端收到并处理完成,但连接在我的服务读到那个 200 之前断掉。
- 我的服务用同一个幂等键重试。
- 接收端是创建了第二笔订单,还是识别出这个键并回答「已经处理过了」?
再多的随机故障也无法按需复现第 2 步。所以这个端点现在有了第三种模式,也是我会最先用的那种。
一份按 key 顺序执行的步骤列表
你给端点一份 JSON 步骤列表。每个不同的 Idempotency-Key(或者你指定的任意 header)都按顺序走完这份列表:同一个 key 的第一次调用拿到第一步,第二次调用拿到第二步,换一个新 key 则从第一步重新开始。
{
"key_header": "Idempotency-Key",
"on_end": "repeat_last",
"steps": [
{ "action": "drop" },
{ "action": "respond", "status": 200, "body": "{\"received\":true}" }
]
}
共三种动作:
respond:一个状态码、一个响应体和可选的delay_ms,各项默认沿用端点的常规设置drop:把请求记入日志,然后不交付响应体直接切断连接timeout:将连接挂起delay_ms(最长 30 秒),然后切断
上面这个配置就是「重复副作用」测试。首次投递:记录日志、切断连接。带同一个 key 的重试:返回 200。如果你的发送方行为正确,日志里同一个 key 的请求应恰好是两条,而数据库里只有一笔订单。
on_end 决定走完最后一步之后发生什么。默认值(repeat_last)会把最后一步永远重复下去,这对正确的发送方正合适:一旦拿到 200 就停止,即便它仍然重试,也只会不断收到 200。loop 则从第一步重新开始,适合浸泡测试(soak test)。
我第一版做错的几件事
key 在存储前会先做哈希。 header 由你来选,因此值可能是订单号、邮箱或任何东西。计数表只需要区分不同的 key,所以只保存哈希值。另外,你不能选 Authorization 或 Cookie 作为 key header,表单会直接拒绝。
没有 key 就意味着共用计数器。 如果发送方根本不带这个 header,所有调用共用同一条序列。这通常是发送方的 bug,请求日志会让它现形。
换一份不同的序列保存端点时,计数器会重置,此外还有一个「Reset sequence」按钮,以及供 CI 任务开头调用的 DELETE /v1/endpoints/{id}/sequence。七天没人动过的计数器会被清除。
每个响应都携带 `X-Supercrontab-Sequence-Step` 响应头,这样测试失败时你就能看出发送方当时走到了第几步。
如实交代的局限
这些端点运行在 Cloudflare Workers 上。在那里,我可以在响应体传输中途切断连接,但无法在状态行发出之前切断。所以 drop 的实际表现像一个被截断的响应:curl 以退出码 18 结束,Python 抛出 IncompleteRead,Go 读取 body 时返回错误。它并不是 TCP reset。在我试过的每一个 HTTP 客户端里,这种失败都会落入同一条「请求失败,重试它」的路径——而这正是被测试的那条路径。如果你的客户端把截断的 200 当作成功,那本身就是一条有价值的发现。
鉴权依然先行。带无效 token 的调用会收到 401,且不消耗步骤,因此一个手忙脚乱配错凭证的发送方不会意外吃掉你正等着观察的那个 drop。
费用
免费套餐一分钱不收:3 个端点、每分钟 30 次请求、每天 500 次,每条序列最多 20 个步骤。含 timeout 步骤的序列会把一次连接挂起最长 30 秒,而这只算一次请求。
API 参考文档在 https://supercrontab.com/docs#sequence,表单入口在 https://supercrontab.com/webhook-tester。如果还缺哪种步骤类型(已经有人提过「在两步之间随机选一个」),欢迎留言说说。
原文:https://dev.to/emma_teelylabs/testing-webhook-retries-deterministically-a-fault-sequence-per-idempotency-key-1k19(作者 @emma_teelylabs)