丢信:每一跳的保证

更新于 · 在 sijie.xyz 查看原条目 ↗

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

从写入到送达,每一跳都要说清楚“掉了会怎样”。原则:至少一次,重复由幂等消化;真正送不到的进 discarded、告警、可手动重试,不存在静默丢失。

每一跳

flowchart LR
  W[business write] -->|① same transaction| O[(outbox)]
  O -->|② claim by row + enqueue in same tx| J[(jobs)]
  J -->|③ worker runs| H[subscriber / webhook]
  H -->|④ receiver accepts| C[consumer]
  S[1-minute relay sweep] -.->|wakes the relay if a NOTIFY was lost| O

每一跳的保证

跳 掉了会怎样 保证
① 写入 → 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。