taskQueue.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. package taskQueue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "fwh-toolbox-server/cache"
  7. "fwh-toolbox-server/domain"
  8. "fwh-toolbox-server/dto"
  9. "fwh-toolbox-server/logger"
  10. "slices"
  11. "time"
  12. "github.com/redis/go-redis/v9"
  13. "go.uber.org/zap"
  14. )
  15. var (
  16. ctx = context.Background()
  17. streamName = "fwhtoolbox:taskQueueStream"
  18. groupName = "task_queue"
  19. consumerID = "worker-taskQueue"
  20. // retryConsumerID = "retry-taskQueue"
  21. retryConsumerID = consumerID
  22. maxRetry = 3
  23. idleTime = 10 * time.Minute
  24. // 状态管理相关变量
  25. runningID = "" // 当前执行的任务ID
  26. failedTaskList = []string{} // 失败且未处理任务ID列表
  27. errorTaskList = []redis.XMessage{} // 超过重试次数仍失败的任务列表
  28. )
  29. type TaskStreamInfo struct {
  30. Ctx context.Context
  31. StreamName string
  32. GroupName string
  33. ID string
  34. }
  35. type TaskPayloadBase struct {
  36. Type string `json:"type"`
  37. }
  38. type TaskPayload[T any] struct {
  39. TaskPayloadBase
  40. Payload T `json:"payload"`
  41. }
  42. func InitTaskQueue() {
  43. // 确保消费组存在(忽略已存在错误)
  44. _ = cache.XGroupCreate(ctx, streamName, groupName, "$")
  45. // 启动后台任务队列(不会阻塞主线程)
  46. go startTaskQueueWorker()
  47. }
  48. // 任务消费者(单 worker,顺序执行)
  49. func startTaskQueueWorker() {
  50. streamsMap := make(map[string]string)
  51. streamsMap[streamName] = ">"
  52. for {
  53. // 处理新任务
  54. res, err := cache.XReadGroup(ctx, groupName, consumerID, streamsMap, 1) // 等待任务
  55. if err != nil {
  56. if err == redis.Nil {
  57. // 未读取到新消息
  58. // 处理滞留的消息
  59. retryRes, _, err := cache.XAutoClaim(ctx, streamName, groupName, retryConsumerID, idleTime, 1)
  60. if err != nil {
  61. if err == redis.Nil {
  62. continue
  63. }
  64. logger.Std.Println("XAutoClaim error:", err)
  65. time.Sleep(time.Second)
  66. continue
  67. }
  68. for _, msg := range retryRes {
  69. pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
  70. if err != nil {
  71. logger.Std.Println("XPendingExtOne error:", err)
  72. } else if pendingRes == nil {
  73. panic("无法读取XPendingExtOne")
  74. }
  75. // 判断重试次数
  76. if pendingRes.RetryCount > int64(maxRetry) {
  77. if slices.Contains(failedTaskList, msg.ID) {
  78. failedTaskList = slices.DeleteFunc(failedTaskList, func(s string) bool { return s == msg.ID })
  79. }
  80. errorTaskList = append(errorTaskList, msg)
  81. ackTask(TaskStreamInfo{
  82. Ctx: ctx,
  83. StreamName: streamName,
  84. GroupName: groupName,
  85. ID: msg.ID,
  86. }, true, fmt.Errorf("任务超过最大重试次数。"))
  87. continue
  88. }
  89. zap.L().Warn("taskQueue任务重试。", zap.String("ID", msg.ID), zap.Int64("count", pendingRes.RetryCount))
  90. runningID = msg.ID
  91. handleTaskQueueMsg(msg)
  92. }
  93. continue
  94. }
  95. logger.Std.Println("XReadGroup error:", err)
  96. time.Sleep(time.Second)
  97. continue
  98. }
  99. for _, stream := range res {
  100. for _, message := range stream.Messages {
  101. runningID = message.ID
  102. handleTaskQueueMsg(message)
  103. }
  104. }
  105. }
  106. }
  107. // 处理单条消息
  108. func handleTaskQueueMsg(message redis.XMessage) {
  109. id := message.ID
  110. rawPayload := message.Values["payload"].(string)
  111. // 先反序列化成 map 确定 type
  112. var base TaskPayloadBase
  113. if err := json.Unmarshal([]byte(rawPayload), &base); err != nil {
  114. ackTask(TaskStreamInfo{
  115. Ctx: ctx,
  116. StreamName: streamName,
  117. GroupName: groupName,
  118. ID: id,
  119. }, false, fmt.Errorf("unmarshal base error - %s", err.Error()))
  120. return
  121. }
  122. switch base.Type {
  123. case TaskTypeSiteAnalysis: // 站点分析任务
  124. var payload TaskPayload[domain.TaskSiteAnalysis]
  125. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  126. ackTask(TaskStreamInfo{
  127. Ctx: ctx,
  128. StreamName: streamName,
  129. GroupName: groupName,
  130. ID: id,
  131. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  132. return
  133. }
  134. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  135. if err := handleTaskSiteAnalysis(ctx, payload.Payload); err != nil {
  136. ackTask(TaskStreamInfo{
  137. Ctx: ctx,
  138. StreamName: streamName,
  139. GroupName: groupName,
  140. ID: id,
  141. }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
  142. return
  143. }
  144. case TaskTypeShutdownStatistics: // Shutdown统计任务
  145. var payload TaskPayload[domain.TaskShutdownStatistics]
  146. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  147. ackTask(TaskStreamInfo{
  148. Ctx: ctx,
  149. StreamName: streamName,
  150. GroupName: groupName,
  151. ID: id,
  152. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  153. return
  154. }
  155. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  156. if err := handleTaskAlarmStatistics(ctx, payload.Payload); err != nil {
  157. ackTask(TaskStreamInfo{
  158. Ctx: ctx,
  159. StreamName: streamName,
  160. GroupName: groupName,
  161. ID: id,
  162. }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
  163. return
  164. }
  165. default:
  166. logger.Std.Println("Unknown task type:", base.Type)
  167. }
  168. // 任务完成
  169. ackTask(TaskStreamInfo{
  170. Ctx: ctx,
  171. StreamName: streamName,
  172. GroupName: groupName,
  173. ID: id,
  174. }, true, nil)
  175. }
  176. func ackTask(streamInfo TaskStreamInfo, success bool, err error) {
  177. if success {
  178. // 确认任务完成
  179. if err := cache.XAckDel(streamInfo.Ctx, streamInfo.StreamName, streamInfo.GroupName, streamInfo.ID); err != nil {
  180. logger.Std.Println("XAck error:", err)
  181. }
  182. } else {
  183. if !slices.Contains(failedTaskList, streamInfo.ID) {
  184. failedTaskList = append(failedTaskList, streamInfo.ID)
  185. }
  186. // 任务失败
  187. logger.Std.Println("Task queue execution failed, error:", err)
  188. }
  189. if err != nil {
  190. zap.L().Error("taskQueue ackTask error.", zap.String("err", err.Error()))
  191. }
  192. }
  193. func EnqueueTask[T any](payload TaskPayload[T]) error {
  194. data, err := json.Marshal(payload)
  195. if err != nil {
  196. return err
  197. }
  198. if _, err := cache.XAdd(ctx, streamName, map[string]interface{}{"payload": string(data)}); err != nil {
  199. return err
  200. }
  201. return nil
  202. }
  203. func GetTaskInfoList() ([]dto.BackgroundTaskItem, error) {
  204. c, cancel := context.WithTimeout(ctx, 15*time.Second)
  205. defer cancel()
  206. allMsg, err := cache.XRangeN(c, streamName, "-", "+", 1000) // 查询所有信息,限制1000条
  207. if err != nil {
  208. zap.L().Error("XRangeN查询失败", zap.String("err", err.Error()))
  209. return nil, err
  210. }
  211. res := []dto.BackgroundTaskItem{}
  212. for _, msg := range append(errorTaskList, allMsg...) {
  213. // 判断任务状态
  214. taskStatus := "pending" // 默认为pending
  215. if msg.ID == runningID {
  216. taskStatus = "running"
  217. } else if slices.Contains(failedTaskList, msg.ID) {
  218. taskStatus = "failed"
  219. }
  220. // 获取任务重试次数
  221. var retryCount int64
  222. pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
  223. if err != nil {
  224. zap.L().Error("XPendingExtOne查询失败", zap.String("err", err.Error()))
  225. return nil, err
  226. } else if pendingRes == nil {
  227. retryCount = 0
  228. } else {
  229. retryCount = pendingRes.RetryCount
  230. }
  231. // 获取任务参数
  232. payload, ok := msg.Values["payload"].(string)
  233. if !ok {
  234. payload = fmt.Sprint(msg.Values["taskId"])
  235. }
  236. item := dto.BackgroundTaskItem{
  237. Id: msg.ID,
  238. Status: taskStatus,
  239. Retry: retryCount,
  240. Payload: payload,
  241. }
  242. res = append(res, item)
  243. }
  244. return res, nil
  245. }