丢信:每一跳的保证
上级:events
状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。
从写入到送达,每一跳都要说清楚“掉了会怎样”。原则:至少一次,重复由幂等消化;真正送不到的进 discarded、告警、可手动重试,不存在静默丢失。
每一跳
每一跳的保证
| 跳 | 掉了会怎样 | 保证 |
|---|---|---|
| ① 写入 → outbox | 事务回滚则业务和事件一起消失 | 不可能“改了没事件”或“有事件没改” |
| ② outbox → 任务 | relay 中途崩溃 | 领取、入队、打标记同一事务;崩了就整体回滚,重启后重领 |
| ② 唤醒丢了 | 监听重连期间丢了一条 NOTIFY | 1 分钟的 events relay sweep 会戳醒 relay;代价是延迟,不丢事件 |
| ② 某行反复失败 | relay 在同一行上一直出错 | 逐行重试;失败 5 次就隔离、放到一边并触发 events_poisoned;永远不会被删掉 |
| ③ 任务 → 执行 | worker 跑到一半被杀 | 任务卡在 running 超过阈值会被救回重试(River rescuer);重复执行由幂等消化 |
| ③ 持续失败 | 对方一直挂 | 退避到上限 → discarded → 告警 + 面板可手动重试,不静默 |
| ④ 对方收了却丢了 | 消费方返回 200 但自己没处理好 | 我们无法控制。owner 可以用 events.list 查看事件流。不跑定时对账 |
| 重复 | 至少一次必然有重复 | webhook-id = 事件 id,消费方按它去重;进程内订阅方幂等 |
| 邮件 | SMTP 250 只是“已接收”不是“已送达” | Message-ID 是 <事件 id@standmeet>;退信处理不在本计划范围 |
| 给 owner 的通知超过突发上限 | 同一 owner 每小时超过 5 条访客申请 | 有意丢弃并记日志,洪水不会每次都给 owner 发信;每条申请在后台都看得见。重试保留自己的名额 |
| 没有邮件 supplier | 永远发不出去 | 任务完成并记日志,不告警 |
| 数据库本身丢数据 | 磁盘故障 | 和业务数据同一套备份(backup.sh),不额外承诺 |
第 ② 跳在设计审查中出过一个真的丢信 bug:最初的 relay 按序号游标读 outbox,交错提交的事务会永远落在游标后面。修法是按行领取、不推游标:relay-claims-rows-not-cursor。
重复是代价,幂等来付
- webhook:每次重试带同一个
webhook-id(= 事件 id),消费方按它去重。 - 进程内订阅方必须幂等。一条注册表驱动的 UT 给每个已注册订阅方投递同一事件两次,断言效果只发生一次(no-bypass-by-structure)。
- 搜索索引:upsert 天然幂等。
- 邮件:SMTP 没有幂等键。顺序是“先发送,再标记”(
notified_at、replied,或删掉预约通知行),发完还没标记就崩,重试会多发一封。邮件带Message-ID<事件 id@standmeet>,多数邮箱会把同 ID 的重复合并。这是至少一次的代价,远比原来“失败即丢”好。
重试怎么排期、怎么分类,见 retry-has-one-owner。