2026-09-27·by Sijie Wang#standmeet#architecture#design#events

retry-has-one-owner

重试只有一个主人:任务层

上级:events

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

重试只有一个主人:任务层。handler 只负责把失败分类,什么时候再试由声明在任务种类上的策略决定。

之前的多层重试 vs 之后

3 × 3 × 3 = 一次失败打出过 27 个请求。现在只有任务层重试:

  • internal/infra/retry 和 notifyPolicy 已删除。门禁 check-retry-only-in-jobs.sh 禁止 retry.Do 出现在 internal/infra/jobs 之外(见 no-bypass-by-structure)。
  • webhook 投递用带 NoRetry 的 httpx。
  • Meili 客户端自带的重试已关掉(meilisearch.DisableRetries()),改由 corpus.index 任务重试。

同步调用只发一次

访客在等的调用(日历忙闲、经 openapi 适配器的插入和删除)不再在请求里跑 retry.Do。暂时性失败立刻映射为 ErrCalendarUnavailable。理由:重试只有一个主人,面向访客的调用要快速回答。真正需要重试的(比如补偿性删除日历事件)是持久的 supplier.invoke 任务。

失败分类:handler 返回什么

  • 4xx 除 408 / 429 外一律视为永久性:再试也不会好,只会刷对方。
  • snooze 不消耗重试次数。webhook 按 Retry-After 的 snooze 最长 10 小时。
  • 在 River 上,jobs.Discard(err) 是带错误的取消,显示为 discarded;面板里手动取消显示为 cancelled。

策略(声明在任务种类上,是数据)

种类最多尝试退避耗尽后
corpus.index、corpus.reindex10DefaultBackoff:1 秒起翻倍,封顶 15 分钟discarded + 告警;面板可重试
owner.notify、access_request.approval_mail、owner.email_confirmation820 秒起,每次 ×3:共约 6 小时discarded + 告警(告警走面板,不再发邮件)
webhook.fanout10DefaultBackoffdiscarded + 告警
webhook.deliver18Svix:5s · 5m · 30m · 2h · 5h · 10h · 10h…(约 5.3 天)discarded;端点连续失败 5 天则停用
周期任务不重试等下一个周期面板显示上次失败

所有退避都加 ±10% 抖动,避免一批同时失败的任务同一时刻一起重来。

对同一个挂掉的端点不要狂敲

  • 任何非成功都会设 failing_since(410 和 429 也算);一次成功就清掉。
  • 每个端点同时只有一次投递,靠一行租约(busy_until);见 concurrency-control。

有副作用的重试:会不会发两次

  • webhook: 每次重试带同一个 webhook-id(= 事件 id),消费方按它去重。见 webhooks。
  • 邮件: SMTP 没有幂等键。顺序是“先发送,再标记”(notified_at、replied,或删掉预约通知行),发完还没标记就崩,重试会多发一封。这是至少一次的代价,远比“失败即丢”好;邮件带 Message-ID <事件 id@standmeet>,多数邮箱会把同 ID 的重复合并。
  • 邮件限流: 每个收件人的额度(每小时 30 封)用完就 snooze 到下个窗口,不丢。给 owner 的通知突发上限(每个 owner 每小时 5 封)仍是有意丢弃;见 message-loss-guarantees。
  • 搜索索引: upsert 天然幂等。

手动重试

tasks-panel 支持单条重试;webhooks.redeliver 在对方修好之后把这个端点所有 discarded 的投递重投一遍。已覆盖:Phase 2 的“sink 连返 500 两次”、“返回 429 + Retry-After → 稍后重试且不计次数”、“返回 410 → 立即 discarded”。

相关:message-loss-guarantees · saturation-degrades-gracefully · concurrency-control

retry-has-one-owner