fix(api): 修复邮件队列超时并补齐调度进程
延长 SendEmailJob 超时并改为超时直接失败,补充重试退避、 失败日志与收件人脱敏,避免 send_email 队列批量超时重试。 新增 MAIL_TIMEOUT 与 QUEUE_RETRY_AFTER 配置,并抽出邮件运行时 配置与 HTML 内容服务,确保 Horizon 常驻进程使用最新邮件配置。 为 Docker、supervisor 与 compose 样例补齐 scheduler 进程,并在 节点管理端开启墙检测托管时立即触发一次检测,保证定时任务持续生效。
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"status":"completed","completed":5,"failed":0,"pending":0,"total":5,"percent":100,"current":"代码修复、配置调整、测试补充、知识库同步和可用验证已完成;PHP 运行环境缺失导致语法检查与 PHPUnit 待补跑","updated_at":"2026-04-28 13:11:00"}
|
||||
@@ -0,0 +1,160 @@
|
||||
# 变更提案: fix-send-email-job-timeout
|
||||
|
||||
## 元信息
|
||||
```yaml
|
||||
类型: 修复
|
||||
方案类型: implementation
|
||||
优先级: P1
|
||||
状态: 已确认
|
||||
创建: 2026-04-28
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求
|
||||
|
||||
### 背景
|
||||
Horizon 失败作业中共有 59 条 `send_email` 队列失败,作业均为 `App\Jobs\SendEmailJob`,错误摘要均为 `Illuminate\Queue\TimeoutExceededException: App\Jobs\SendEmailJob has timed out.`。代码排查发现 `SendEmailJob` 当前 `$timeout = 10`,明显短于生产环境 SMTP 连接、TLS 握手和邮件服务商慢响应的常见耗时,也短于 `config/horizon.php` 中 notification supervisor 的 60 秒 timeout。
|
||||
|
||||
### 目标
|
||||
- 降低正常邮件发送因 10 秒 job 超时导致失败的概率。
|
||||
- 让邮件发送失败走 Laravel 队列异常/重试机制,避免手动 `release()` 隐藏真实失败原因。
|
||||
- 给 SMTP 传输设置明确超时,避免单次发送无限阻塞直到 worker 超时。
|
||||
- 保留邮件失败可观测性,同时避免将邮件密码等敏感配置写入日志或数据库。
|
||||
|
||||
### 约束条件
|
||||
```yaml
|
||||
时间约束: 当前回合内完成代码修复和可执行验证。
|
||||
性能约束: 不引入同步批量发信,不扩大单个 worker 的无限等待时间。
|
||||
兼容性约束: 保持现有 Laravel 12 + Horizon 队列结构,继续兼容当前 legacy mail config。
|
||||
业务约束: 不直接清空、重试或修改生产 Horizon 失败作业;生产重试需要另行确认。
|
||||
```
|
||||
|
||||
### 验收标准
|
||||
- [ ] `SendEmailJob` 的 job timeout 不再是 10 秒,并且小于 Redis `retry_after`。
|
||||
- [ ] 邮件发送返回错误时抛出异常,由队列统一处理 retries/backoff/fail,而不是直接 `release(60)`。
|
||||
- [ ] SMTP 配置包含可调 `MAIL_TIMEOUT`,运行时邮件配置能在 Horizon 长驻 worker 中生效。
|
||||
- [ ] `MailLog` 中保存的邮件配置不包含明文 `password`。
|
||||
- [ ] 新增或更新测试覆盖 job 超时配置、失败抛出、邮件配置脱敏。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 技术方案
|
||||
采用唯一修复路径:保留现有 `SendEmailJob` 和 `MailService` 架构,不拆分新队列、不引入新依赖。将 `SendEmailJob` 调整为更适合 SMTP 的超时和 backoff 策略;邮件发送失败时抛出异常交给 Laravel Queue/Horizon;在 `MailService` 中统一应用运行时邮件配置、设置 SMTP timeout、刷新 Laravel MailManager 缓存,并对写入 `MailLog` 的配置做敏感字段脱敏。
|
||||
|
||||
### 影响范围
|
||||
```yaml
|
||||
涉及模块:
|
||||
- queue-mail: SendEmailJob 的超时、重试、失败处理行为。
|
||||
- mail-service: 运行时 SMTP 配置、发送日志、敏感信息脱敏。
|
||||
- config: MAIL_TIMEOUT 与队列 retry_after 可配置化。
|
||||
预计变更文件: 5-7
|
||||
```
|
||||
|
||||
### 风险评估
|
||||
| 风险 | 等级 | 应对 |
|
||||
|------|------|------|
|
||||
| 超时时间过长导致 worker 被慢 SMTP 占用 | 中 | SMTP transport timeout 默认 30 秒,job timeout 60 秒,仍小于 retry_after。 |
|
||||
| 失败后抛异常可能让失败摘要变化 | 低 | 这是期望行为,能暴露真实邮件错误;超时仍由 Horizon 记录。 |
|
||||
| 生产已有失败作业重试后可能重复发信 | 中 | 本方案不自动重试历史失败作业,生产重试需按作业内容另行确认。 |
|
||||
| 运行时刷新 MailManager 影响同 worker 后续邮件 | 低 | 仅在每次发信前应用当前配置,符合动态后台邮件配置的预期。 |
|
||||
|
||||
### 方案取舍
|
||||
```yaml
|
||||
唯一方案理由: 故障根因集中在当前 job 过短 timeout 和邮件发送失败处理方式,局部修复能直接降低失败率且不改变业务入口。
|
||||
放弃的替代路径:
|
||||
- 单纯把 Horizon notification timeout 调大: 无法覆盖 job 自身 timeout=10,也不能解决 SMTP 无限阻塞和错误摘要不清晰。
|
||||
- 新增邮件服务商 API SDK: 需要新的凭据、配置和迁移成本,超出当前故障修复范围。
|
||||
- 自动重试全部失败作业: 可能造成重复邮件,属于生产副作用,不在本次代码修复中执行。
|
||||
回滚边界: 可独立回退 SendEmailJob、MailService、config 与测试变更;不会修改数据库结构。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术设计
|
||||
|
||||
### 核心流程
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[SendEmailJob handle] --> B[MailService::sendEmail]
|
||||
B --> C[应用运行时 mail 配置和 timeout]
|
||||
C --> D[刷新 MailManager 缓存]
|
||||
D --> E[发送邮件并写入 MailLog]
|
||||
E -->|success| F[Job completed]
|
||||
E -->|error| G[SendEmailJob 抛 RuntimeException]
|
||||
G --> H[Queue retries/backoff/fail]
|
||||
```
|
||||
|
||||
### 配置边界
|
||||
- `MAIL_TIMEOUT` 控制 SMTP transport timeout,默认 30 秒。
|
||||
- `SendEmailJob::$timeout` 控制单个 job 最大执行时长,计划设置为 60 秒。
|
||||
- `queue.redis.retry_after` 保持大于 job timeout;改为可通过 `QUEUE_RETRY_AFTER` 调整,默认 90 秒。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心场景
|
||||
|
||||
### 场景: 单封邮件发送成功
|
||||
**模块**: queue-mail
|
||||
**条件**: SMTP 服务在 `MAIL_TIMEOUT` 内响应成功。
|
||||
**行为**: `SendEmailJob` 调用 `MailService::sendEmail()`。
|
||||
**结果**: job 正常完成,`MailLog.error = null`。
|
||||
|
||||
### 场景: SMTP 返回错误
|
||||
**模块**: queue-mail
|
||||
**条件**: SMTP 认证失败、连接失败或服务商返回错误。
|
||||
**行为**: `MailService` 记录脱敏配置和错误摘要,`SendEmailJob` 抛出异常。
|
||||
**结果**: Horizon 按 job tries/backoff 重试,失败摘要展示真实错误。
|
||||
|
||||
### 场景: SMTP 长时间无响应
|
||||
**模块**: queue-mail
|
||||
**条件**: SMTP 连接或读写超过 `MAIL_TIMEOUT`。
|
||||
**行为**: 邮件传输超时后返回错误,若仍阻塞则 job timeout 兜底。
|
||||
**结果**: 单个 job 不会被无限占用,且 `retry_after` 不早于 timeout 触发。
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术决策
|
||||
|
||||
### fix-send-email-job-timeout#D001: 保留队列结构并修复 job 与 mail transport 超时
|
||||
**日期**: 2026-04-28
|
||||
**状态**: ✅采纳
|
||||
**背景**: 失败作业都集中在 `SendEmailJob has timed out`,当前 job timeout 只有 10 秒。
|
||||
**选项分析**:
|
||||
| 选项 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 局部修复 job timeout、mail timeout 和错误处理 | 改动小,直接命中根因,易验证和回滚 | 仍依赖 SMTP 服务商自身稳定性 |
|
||||
| B: 只增大 Horizon timeout | 改动更少 | job 自身 timeout=10 仍会失败,且缺少传输层超时 |
|
||||
| C: 改造为第三方邮件 API | 长期可观测性更好 | 需要新配置和迁移,超出当前故障修复 |
|
||||
**决策**: 选择方案 A。
|
||||
**理由**: 当前故障由代码事实直接定位到 job timeout 过短和失败处理不清晰,局部修复收益最高、风险最低。
|
||||
**影响**: `SendEmailJob`、`MailService`、`config/mail.php`、`config/queue.php`、`.env.example`、相关单元测试。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证策略
|
||||
|
||||
```yaml
|
||||
verifyMode: test-first
|
||||
reviewerFocus:
|
||||
- app/Jobs/SendEmailJob.php 的 timeout/tries/backoff/failOnTimeout 与 retry_after 关系。
|
||||
- app/Services/MailService.php 是否刷新 mailer 缓存并避免明文 password 写入 MailLog。
|
||||
- config/mail.php 与 config/queue.php 是否保持默认值可部署。
|
||||
testerFocus:
|
||||
- vendor/bin/phpunit --filter SendEmailJobTest
|
||||
- vendor/bin/phpunit --filter MailServiceTest
|
||||
- php -l app/Jobs/SendEmailJob.php
|
||||
- php -l app/Services/MailService.php
|
||||
uiValidation: none
|
||||
riskBoundary:
|
||||
- 不执行 horizon:forget、queue:retry、horizon:terminate 等会影响生产队列的命令。
|
||||
- 不读取或修改 .env 中的真实 SMTP 密码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 成果设计
|
||||
|
||||
N/A。此任务不涉及视觉产出。
|
||||
@@ -0,0 +1,85 @@
|
||||
# 任务清单: fix-send-email-job-timeout
|
||||
|
||||
> **@status:** completed | 2026-04-28 13:10
|
||||
|
||||
```yaml
|
||||
@feature: fix-send-email-job-timeout
|
||||
@created: 2026-04-28
|
||||
@status: completed
|
||||
@mode: R2
|
||||
@type: implementation
|
||||
@complexity: moderate
|
||||
```
|
||||
|
||||
## LIVE_STATUS
|
||||
|
||||
```json
|
||||
{"status":"completed","completed":5,"failed":0,"pending":0,"total":5,"percent":100,"current":"代码修复、配置调整、测试补充、知识库同步和可用验证已完成;PHP 运行环境缺失导致语法检查与 PHPUnit 待补跑","updated_at":"2026-04-28 13:11:00"}
|
||||
```
|
||||
|
||||
## 进度概览
|
||||
|
||||
| 完成 | 失败 | 跳过 | 总数 |
|
||||
|------|------|------|------|
|
||||
| 5 | 0 | 0 | 5 |
|
||||
|
||||
---
|
||||
|
||||
## 任务列表
|
||||
|
||||
### 1. 邮件队列执行策略
|
||||
|
||||
- [√] 1.1 修改 `app/Jobs/SendEmailJob.php`
|
||||
- 预期变更: 将 job timeout 从 10 秒提升到适合 SMTP 的值,增加 timeout 失败处理、backoff,并在邮件发送返回错误时抛出异常。
|
||||
- 完成标准: job timeout 大于 `MAIL_TIMEOUT` 默认值且小于 Redis `retry_after` 默认值;失败摘要不再依赖手动 `release(60)`。
|
||||
- 验证方式: `php -l app/Jobs/SendEmailJob.php`;相关单元测试断言 timeout/backoff/失败抛出。
|
||||
- depends_on: []
|
||||
|
||||
### 2. 邮件服务运行时配置
|
||||
|
||||
- [√] 2.1 修改 `app/Services/MailService.php`
|
||||
- 预期变更: 统一应用运行时 SMTP 配置和 timeout,刷新 MailManager 缓存,并对写入 `MailLog` 的 config 脱敏。
|
||||
- 完成标准: `MAIL_TIMEOUT` 能写入 legacy 和 smtp mailer 配置;`password` 不以明文进入 `MailLog.config`。
|
||||
- 验证方式: `php -l app/Services/MailService.php`;相关单元测试断言配置和脱敏行为。
|
||||
- depends_on: []
|
||||
|
||||
### 3. 队列与环境配置
|
||||
|
||||
- [√] 3.1 修改 `config/mail.php`、`config/queue.php` 和 `.env.example`
|
||||
- 预期变更: 增加 `MAIL_TIMEOUT` 默认配置;Redis/database/beanstalkd `retry_after` 支持环境变量,默认值保持大于邮件 job timeout。
|
||||
- 完成标准: 默认 `MAIL_TIMEOUT=30`、`QUEUE_RETRY_AFTER=90`,不会低于 `SendEmailJob::$timeout=60`。
|
||||
- 验证方式: 文件检查;`php -l config/mail.php`;`php -l config/queue.php`。
|
||||
- depends_on: [1.1]
|
||||
|
||||
### 4. 测试覆盖
|
||||
|
||||
- [√] 4.1 新增邮件队列相关单元测试
|
||||
- 预期变更: 覆盖 `SendEmailJob` 的 timeout/backoff/失败抛出,以及 `MailService` 邮件配置脱敏。
|
||||
- 完成标准: 测试不依赖真实 SMTP,不读取真实 `.env` 密码。
|
||||
- 验证方式: `vendor/bin/phpunit --filter SendEmailJobTest`;`vendor/bin/phpunit --filter MailServiceTest`。
|
||||
- depends_on: [1.1, 2.1, 3.1]
|
||||
|
||||
### 5. 验收与知识库同步
|
||||
|
||||
- [√] 5.1 执行验证并同步知识库
|
||||
- 预期变更: 运行可用的语法检查/单测,更新 `.helloagents` 知识库和方案包状态。
|
||||
- 完成标准: 验证结果记录在执行日志;相关知识库模块或 CHANGELOG 反映本次修复。
|
||||
- 验证方式: 查看命令输出、`tasks.md` 状态和 `.helloagents/CHANGELOG.md`。
|
||||
- depends_on: [4.1]
|
||||
|
||||
---
|
||||
|
||||
## 执行日志
|
||||
|
||||
| 时间 | 任务 | 状态 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 2026-04-28 13:11 | 5.1 | completed | 已完成 diff 检查、方案包校验、知识库同步;本地缺少 php/composer/docker,PHP 语法检查和 PHPUnit 待在目标环境补跑 |
|
||||
| 2026-04-28 13:08 | 1.1-4.1 | completed | 已完成邮件 job、运行时邮件配置、环境配置和单测补充;本地缺少 php/composer,自动化执行待补跑 |
|
||||
| 2026-04-28 12:58 | DESIGN | completed | 已完成上下文收集和唯一方案规划 |
|
||||
|
||||
---
|
||||
|
||||
## 执行备注
|
||||
|
||||
- 生产历史失败作业不在本方案内自动重试,避免重复邮件。
|
||||
- 当前环境未安装 `php`、`composer` 或 `docker`,无法执行 `php -l` 与 PHPUnit;已完成静态 diff 检查、方案包校验和人工代码审查,仍需在具备 PHP/Composer 的环境补跑命令。
|
||||
Reference in New Issue
Block a user