Skip to content

plugin-email: 邮件投递接入持久化队列 —— send 走 email.send.async / sys_job_queue(重试+DLQ),可配置开关 #5160

Description

@os-zhuang

维护者已拍板要队列(2026-08-04,PM 会话内决策)。本单是接线,不是造基建 —— 三块部件都已存在,缺的是把它们连起来并给出显式开关。

现状(#5087 落地后)

投递有三层,前两层同步、第三层无人使用:

  1. SmtpTransport.send() 同步跑完 SMTP 会话(超时默认 20s×3 段),调用方 await 里完成;
  2. EmailService.deliverNormalized 进程内重试:retries + 1 次、指数退避封顶 2000ms、全在同一个 await 里,进程一死重试即消失;sys_email.statusqueued 只是发送前一瞬的预备状态,failed 是终态、无人再捞;
  3. EmailServicePlugin 已订阅 email.send.async(handler 里 send() 返回 failed 即抛、交队列重试/DLQ),service-queue 的 DB adapter + sys_job_queue 有完整测试(退避重试、maxAttempts 耗尽转 dlqlistFailed)——但仓内没有任何生产者 publish 到这个主题,直接调 IEmailService.send() 永远走内联。

目标

默认行为不变(内联投递);新增队列投递模式,开启后 send() 的语义变为:

  1. 先落 sys_email 行(status: 'queued')—— 与现在相同;
  2. publish('email.send.async', { rowId }, { maxAttempts, backoff }),引用已落的行;
  3. 立即返回 { id, status: 'queued' } —— 'queued' 已在 EmailDeliveryStatus 枚举里,因此不需要碰 packages/spec;
  4. 队列 worker 用 deliverPersistedRow(row) 投递,把同一行推进到 sent / failed;maxAttempts 耗尽由队列转 DLQ。

必须一并修的既有缺陷:现订阅者的重复插行

现在的 email.send.async handler 是 svc.send(msg.data) —— 每次队列重试都会插一条新的 sys_email。改为按 rowIddeliverPersistedRow,一信一行,attempt_count 累计在同一行上。(兼容:老消息若带的是 sendInput 而非 rowId,按旧路径处理一个迁移窗口,由实现判断。)

配置门(与 #5087 三门同构)

边界语义(PM 裁定,可反驳)

不在本单

  • sys_email 存量 queued 行的启动清扫 / drain 钩子吞错 —— 另单(同文件,串行在本单之后)。
  • packages/spec('queued' 已在枚举,无需动;车道另有四单在飞)、⛔ content/docs/releases/

验收

  • 开启队列模式:send() 立返 queued,worker 异步推进同一行到 sent(含 message_id);SMTP 535 时队列按 backoff 重试、耗尽转 DLQ,sys_email 行终态 failed 且 error 在案;
  • 默认(未开启):行为与今天逐字节一致,现有 236+ 用例不改一条断言;
  • 设置页开关热生效;队列服务缺失的两路语义各有用例;
  • 现订阅者重复插行的缺陷有回归测试钉死(同一消息重试 N 次 = 1 行,attempt_count 累计)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions