JianFeeeee
34a15dc09c
fix(网关): 占住幂等键后的早退路径没退键 —— 永久占键、重试永远 duplicate
## 缺陷
`ClaimRelay` 成功之后、邮件落库之前有若干条早退路径,只有**部分**在 return 前
调了 `ReleaseRelay`。漏掉的那些会把 (agent_name, relay_key) 永久占住:
之后同一上游消息的重试拿到 `ErrRelayDuplicate` ⇒ 返回 200 `duplicate_relay`
(文案是「该上游消息已转发过,本次调用未产生新邮件」),
而那条消息**从未发出去** —— 响应在陈述一件没发生的事。
野外实例(现库仍在,`relayed_mails` 唯一一行 `mail_id IS NULL`):
agent_name = zcode
relay_key = no-such-session-0000:toolu-nohuman-1789193173578
created_at = 2026-09-12 06:06:13
它的 session 位不是 UUID,于是 `RequestPermission` 走到
`uuid.Parse` 失败那条 `Invalid session_id`(permission.go:112)直接 return,
没有退键。该行从 09-12 留到现在。
`mail.go` 同类:`ClaimRelay`(:396)之后 `ConsumeSessionBudget` 返回
非「额度耗尽」错误时(:433)也没退键。
## 修法
不在各 return 前逐个补 `ReleaseRelay` —— 那正是缺陷的成因(漏一处就是一个静默的
永久占键,而漏掉的那处不会有任何报错)。改为占键成功后立刻 `defer` 一次释放,
让"还键"成为该作用域的唯一出口不变量。
成功路径不需要额外开关:`ReleaseRelay` 的 WHERE 含 `mail_id IS NULL`,
`BindRelayMail` 一旦成功该行就不再匹配,defer 自然什么都不做。
## 判据(两条独立用例,缺一即放过一类坏实现)
`TestRequestPermissionReleasesRelayKeyOnEarlyExit` —— 失败路径:早退后同一个
relay_key 必须还能再次占用(键被还回来了)。
`TestRequestPermissionClaimsRelayKeyOnSuccess` —— 成功路径:同一询问重复投递
必须被幂等键挡住,且键确实绑定到了第一封邮件上。
两条必须分开:早退时"正确实现"与"把 ClaimRelay 换成空操作"的坏实现在表里
**观测等价**(都留下一个空键),单条用例无法同时钉住"该退的时候退"和
"该占的时候占"。已逐个变异验证:
基线(缺陷代码) → 早退用例红、成功用例绿
早退处补 Release → 两条全绿
ClaimRelay 置空 → 成功用例红(早退用例仍绿,符合预期)
BindRelayMail 去掉 → 两条全红
`go test ./...` 13 包全绿;handler 组 -race 通过;gofmt/vet 干净。
2026-09-25 05:41:57 +08:00
..
2026-09-18 11:23:12 +08:00
2026-09-25 05:41:57 +08:00
2026-09-19 12:30:05 +08:00
2026-09-08 19:16:35 +08:00