fwhQueueNew 是使用 Asynq 框架重写的 FWH 任务队列,替代原有基于 Redis Stream 手动实现的 fwhQueue。
.String() vs .Hex() 导致任务状态不一致)for 循环)task/queue/fwhQueueNew/
├── fwhTaskQueue.go # 初始化:Server、Client、Inspector、常量定义
├── fwhTaskBatchDownload.go # BatchDownload 主任务 handler
├── fwhTaskCallFile.go # CallFile 子任务 handler(EmsLog + FaultRecord)
├── fwhTaskSiteAnalysis.go # SiteAnalysis 任务 handler
├── fwhTaskShutdownStatistics.go # ShutdownStatistics 任务 handler
├── fwhTaskSiteInfoUpdate.go # SiteInfoUpdate 任务 handler
├── fwhEnqueue.go # 对外暴露的入队函数
└── DESIGN.md # 本文档
┌─────────────────────────────────────────────────────────┐
│ Asynq Server │
│ Concurrency: 6 │
│ │
│ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ fwh:main (2) │ │ fwh:download (4) │ │
│ │ │ │ │ │
│ │ • BatchDownload │ │ • CallFile (EmsLog) │ │
│ │ • SiteAnalysis │ │ • CallFile (FaultRecord) │ │
│ │ • ShutdownStats │ │ │ │
│ │ • SiteInfoUpdate │ │ │ │
│ └──────────────────┘ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
fwh:main:主队列,2 个 worker,处理业务编排类任务fwh:download:下载队列,4 个 worker,处理文件下载类任务(可并发)| 任务类型 | 常量 | 队列 | 重试 | 超时 |
|---|---|---|---|---|
| 批量数据下载 | fwh:BatchDownload |
main | 0 | 120min |
| 文件下载子任务 | fwh:CallFile |
download | 3 | 35min |
| 站点分析 | fwh:SiteAnalysis |
main | 0 | 60min |
| Shutdown统计 | fwh:ShutdownStatistics |
main | 0 | 30min |
| 站点信息更新 | fwh:SiteInfoUpdate |
main | 0 | 60min |
旧方案:Redis Set (standby/failed) + Redis Stream per SN
问题:
1. mainTaskId 使用 .String() vs .Hex() 不一致导致 key 不匹配
2. 提前 return 的路径忘记更新 standby → 主任务永远等待
3. 手动 goroutine per SN 的生命周期管理复杂
新方案:Redis 计数器 + 失败原因列表
入队时:
SET fwh:callfile:counter:{mainTaskId} = N (子任务总数)
子任务完成时(无论成功/失败):
DECR fwh:callfile:counter:{mainTaskId}
如果失败:LPUSH fwh:callfile:failures:{mainTaskId} "失败原因"
主任务轮询:
GET fwh:callfile:counter:{mainTaskId}
当 counter <= 0 时:
LRANGE fwh:callfile:failures:{mainTaskId} 0 -1 → 收集失败原因
DEL counter, failures → 清理
优势:
Asynq 外层重试(MaxRetry=3):
- 第1次失败 → 等待 30s 重试
- 第2次失败 → 等待 2min 重试
- 第3次失败 → 标记为最终失败,报告失败原因
内部下载重试(maxCallRetry=3):
- 针对 HTTP 下载的短暂网络错误
- 不涉及重新召唤,只重试下载动作
业务错误(数据格式错误、API 返回失败等)不重试,handler 返回 nil 避免 Asynq 重试。只有基础设施错误(Redis 断连等)才返回 error 触发重试。
| 维度 | fwhQueue (旧) | fwhQueueNew (新) |
|---|---|---|
| 底层 | 手动 Redis Stream | Asynq(内部用 Redis List + SortedSet) |
| 并发控制 | 单 worker 顺序 + 手动 goroutine per SN | Concurrency + Queues 声明式 |
| 重试 | XAutoClaim + XPendingExt 手动计数 |
MaxRetry(N) + 指数退避 |
| 任务去重 | 手动 Redis 分布式锁 | asynq.Unique(duration) |
| 任务状态 | 内存变量runningID/failedTaskList |
Asynq 自动管理 |
| 子任务协调 | Redis Set (standby/failed) 手动移动 | Redis 计数器 DECR |
| 监控 | 手动GetTaskInfoList |
Asynqmon Web UI |
| Bug 风险 | 高(多种状态同步路径) | 低(结构化保证) |
| 代码量 | ~800 行(fwhTaskQueue.go + callFwhFile.go) | ~500 行(分布在 7 个文件) |
task/queue/taskQueue.gopackage taskQueue
import (
task_core "fwh-toolbox-server/task/core"
"fwh-toolbox-server/task/queue/fwhQueueNew" // 替换 fwhQueue
"fwh-toolbox-server/task/queue/simCardQueue"
"github.com/hibiken/asynq"
)
func InitTaskQueue(tc *task_core.TaskCore, redisOpt asynq.RedisClientOpt) {
if tc == nil {
panic("InitTaskQueue failed, TaskCore is nil.")
}
fwhQueueNew.InitFwhQueueNew(tc, redisOpt) // 替换 fwhQueue.InitFwhTaskQueue()
simCardQueue.InitSimCardTaskQueue(tc)
}
task/task.gopackage task
import (
task_core "fwh-toolbox-server/task/core"
taskQueue "fwh-toolbox-server/task/queue"
"github.com/hibiken/asynq"
)
func InitTask(redisOpt TaskRedisOpt) {
redisClientOpt := asynq.RedisClientOpt{
Addr: redisOpt.Addr,
DB: redisOpt.DB,
Username: redisOpt.Username,
Password: redisOpt.Password,
}
tc := task_core.NewTaskCore(redisClientOpt)
taskQueue.InitTaskQueue(tc, redisClientOpt) // 传入 redisOpt
}
services/batchDownloadServices.go// 将:
import "fwh-toolbox-server/task/queue/fwhQueue"
fwhQueue.EnqueueBatchDownloadTask(...)
// 替换为:
import "fwh-toolbox-server/task/queue/fwhQueueNew"
fwhQueueNew.EnqueueBatchDownloadTask(...)
fwhQueue.EnqueueXxx 的地方全局搜索 fwhQueue.Enqueue 替换为 fwhQueueNew.Enqueue:
services/sitesAnalysisServices.goscheduler/scheduler.go(如果有定时触发 ShutdownStatistics)controllers/ 中直接调用入队的地方main.go 中的 Asynqmon 配置无需改动,Asynqmon 已经指向同一个 Redis DB,新队列的任务会自动出现在 UI 中。
fwh:download 队列的 4 个 worker 可能同时处理不同 SN 的文件下载,这是安全的(每个 SN 独立)Retention(7*24*time.Hour) 保留已完成任务 7 天,方便在 Asynqmon 中查看历史