| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- package taskQueue
- import (
- "context"
- "encoding/json"
- "fmt"
- "fwh-toolbox-server/cache"
- "fwh-toolbox-server/domain"
- "fwh-toolbox-server/dto"
- "fwh-toolbox-server/logger"
- "slices"
- "time"
- "github.com/redis/go-redis/v9"
- "go.uber.org/zap"
- )
- var (
- ctx = context.Background()
- streamName = "fwhtoolbox:taskQueueStream"
- groupName = "task_queue"
- consumerID = "worker-taskQueue"
- // retryConsumerID = "retry-taskQueue"
- retryConsumerID = consumerID
- maxRetry = 3
- idleTime = 10 * time.Minute
- // 状态管理相关变量
- runningID = "" // 当前执行的任务ID
- failedTaskList = []string{} // 失败且未处理任务ID列表
- errorTaskList = []redis.XMessage{} // 超过重试次数仍失败的任务列表
- )
- type TaskStreamInfo struct {
- Ctx context.Context
- StreamName string
- GroupName string
- ID string
- }
- type TaskPayloadBase struct {
- Type string `json:"type"`
- }
- type TaskPayload[T any] struct {
- TaskPayloadBase
- Payload T `json:"payload"`
- }
- func InitTaskQueue() {
- // 确保消费组存在(忽略已存在错误)
- _ = cache.XGroupCreate(ctx, streamName, groupName, "$")
- // 启动后台任务队列(不会阻塞主线程)
- go startTaskQueueWorker()
- }
- // 任务消费者(单 worker,顺序执行)
- func startTaskQueueWorker() {
- streamsMap := make(map[string]string)
- streamsMap[streamName] = ">"
- for {
- // 处理新任务
- res, err := cache.XReadGroup(ctx, groupName, consumerID, streamsMap, 1) // 等待任务
- if err != nil {
- if err == redis.Nil {
- // 未读取到新消息
- // 处理滞留的消息
- retryRes, _, err := cache.XAutoClaim(ctx, streamName, groupName, retryConsumerID, idleTime, 1)
- if err != nil {
- if err == redis.Nil {
- continue
- }
- logger.Std.Println("XAutoClaim error:", err)
- time.Sleep(time.Second)
- continue
- }
- for _, msg := range retryRes {
- pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
- if err != nil {
- logger.Std.Println("XPendingExtOne error:", err)
- } else if pendingRes == nil {
- panic("无法读取XPendingExtOne")
- }
- // 判断重试次数
- if pendingRes.RetryCount > int64(maxRetry) {
- if slices.Contains(failedTaskList, msg.ID) {
- failedTaskList = slices.DeleteFunc(failedTaskList, func(s string) bool { return s == msg.ID })
- }
- errorTaskList = append(errorTaskList, msg)
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: msg.ID,
- }, true, fmt.Errorf("任务超过最大重试次数。"))
- continue
- }
- zap.L().Warn("taskQueue任务重试。", zap.String("ID", msg.ID), zap.Int64("count", pendingRes.RetryCount))
- runningID = msg.ID
- handleTaskQueueMsg(msg)
- }
- continue
- }
- logger.Std.Println("XReadGroup error:", err)
- time.Sleep(time.Second)
- continue
- }
- for _, stream := range res {
- for _, message := range stream.Messages {
- runningID = message.ID
- handleTaskQueueMsg(message)
- }
- }
- }
- }
- // 处理单条消息
- func handleTaskQueueMsg(message redis.XMessage) {
- id := message.ID
- rawPayload := message.Values["payload"].(string)
- // 先反序列化成 map 确定 type
- var base TaskPayloadBase
- if err := json.Unmarshal([]byte(rawPayload), &base); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("unmarshal base error - %s", err.Error()))
- return
- }
- switch base.Type {
- case TaskTypeSiteAnalysis: // 站点分析任务
- var payload TaskPayload[domain.TaskSiteAnalysis]
- if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
- return
- }
- logger.Std.Printf("Processing task %s: %+v\n", id, payload)
- if err := handleTaskSiteAnalysis(ctx, payload.Payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
- return
- }
- case TaskTypeShutdownStatistics: // Shutdown统计任务
- var payload TaskPayload[domain.TaskShutdownStatistics]
- if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
- return
- }
- logger.Std.Printf("Processing task %s: %+v\n", id, payload)
- if err := handleTaskShutdownStatistics(ctx, payload.Payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
- return
- }
- case TaskTypeBatchDownload: // 批量数据下载任务
- var payload TaskPayload[domain.TaskBatchDownload]
- if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
- return
- }
- logger.Std.Printf("Processing task %s: %+v\n", id, payload)
- if err := handleTaskBatchDownload(ctx, payload.Payload); err != nil {
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, false, fmt.Errorf("handle task BatchDownload error - %s", err.Error()))
- return
- }
- default:
- logger.Std.Println("Unknown task type:", base.Type)
- }
- // 任务完成
- ackTask(TaskStreamInfo{
- Ctx: ctx,
- StreamName: streamName,
- GroupName: groupName,
- ID: id,
- }, true, nil)
- }
- func ackTask(streamInfo TaskStreamInfo, success bool, err error) {
- if success {
- // 确认任务完成
- if err := cache.XAckDel(streamInfo.Ctx, streamInfo.StreamName, streamInfo.GroupName, streamInfo.ID); err != nil {
- logger.Std.Println("XAck error:", err)
- }
- } else {
- if !slices.Contains(failedTaskList, streamInfo.ID) {
- failedTaskList = append(failedTaskList, streamInfo.ID)
- }
- // 任务失败
- logger.Std.Println("Task queue execution failed, error:", err)
- }
- if err != nil {
- zap.L().Error("taskQueue ackTask error.", zap.String("err", err.Error()))
- }
- }
- func EnqueueTask[T any](payload TaskPayload[T]) error {
- data, err := json.Marshal(payload)
- if err != nil {
- return err
- }
- if _, err := cache.XAdd(ctx, streamName, map[string]interface{}{"payload": string(data)}); err != nil {
- return err
- }
- return nil
- }
- func GetTaskInfoList() ([]dto.BackgroundTaskItem, error) {
- c, cancel := context.WithTimeout(ctx, 15*time.Second)
- defer cancel()
- allMsg, err := cache.XRangeN(c, streamName, "-", "+", 1000) // 查询所有信息,限制1000条
- if err != nil {
- zap.L().Error("XRangeN查询失败", zap.String("err", err.Error()))
- return nil, err
- }
- res := []dto.BackgroundTaskItem{}
- for _, msg := range append(errorTaskList, allMsg...) {
- // 判断任务状态
- taskStatus := "pending" // 默认为pending
- if msg.ID == runningID {
- taskStatus = "running"
- } else if slices.Contains(failedTaskList, msg.ID) {
- taskStatus = "failed"
- }
- // 获取任务重试次数
- var retryCount int64
- pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
- if err != nil {
- zap.L().Error("XPendingExtOne查询失败", zap.String("err", err.Error()))
- return nil, err
- } else if pendingRes == nil {
- retryCount = 0
- } else {
- retryCount = pendingRes.RetryCount
- }
- // 获取任务参数
- payload, ok := msg.Values["payload"].(string)
- if !ok {
- payload = fmt.Sprint(msg.Values["taskId"])
- }
- item := dto.BackgroundTaskItem{
- Id: msg.ID,
- Status: taskStatus,
- Retry: retryCount,
- Payload: payload,
- }
- res = append(res, item)
- }
- return res, nil
- }
|