fwhTaskQueue.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. package fwhQueue
  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:taskQueue:taskStream"
  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. // 唯一任务key前缀
  29. uniqueTaskKeyPrefix = "fwhtoolbox:taskQueue:taskUnique:"
  30. )
  31. type TaskStreamInfo struct {
  32. Ctx context.Context
  33. StreamName string
  34. GroupName string
  35. ID string
  36. }
  37. type TaskPayloadBase struct {
  38. Type string `json:"type"`
  39. }
  40. type TaskPayload[T any] struct {
  41. TaskPayloadBase
  42. Payload T `json:"payload"`
  43. }
  44. func InitFwhTaskQueue() {
  45. // 确保消费组存在(忽略已存在错误)
  46. _ = cache.XGroupCreate(ctx, streamName, groupName, "$")
  47. // 启动后台任务队列(不会阻塞主线程)
  48. go startTaskQueueWorker()
  49. }
  50. // 任务消费者(单 worker,顺序执行)
  51. func startTaskQueueWorker() {
  52. streamsMap := make(map[string]string)
  53. streamsMap[streamName] = ">"
  54. for {
  55. // 处理新任务
  56. res, err := cache.XReadGroup(ctx, groupName, consumerID, streamsMap, 1) // 等待任务
  57. if err != nil {
  58. if err == redis.Nil {
  59. // 未读取到新消息
  60. // 处理滞留的消息
  61. retryRes, _, err := cache.XAutoClaim(ctx, streamName, groupName, retryConsumerID, idleTime, 1)
  62. if err != nil {
  63. if err == redis.Nil {
  64. continue
  65. }
  66. logger.Std.Println("XAutoClaim error:", err)
  67. time.Sleep(time.Second)
  68. continue
  69. }
  70. for _, msg := range retryRes {
  71. pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
  72. if err != nil {
  73. logger.Std.Println("XPendingExtOne error:", err)
  74. } else if pendingRes == nil {
  75. panic("无法读取XPendingExtOne")
  76. }
  77. // 判断重试次数
  78. if pendingRes.RetryCount > int64(maxRetry) {
  79. if slices.Contains(failedTaskList, msg.ID) {
  80. failedTaskList = slices.DeleteFunc(failedTaskList, func(s string) bool { return s == msg.ID })
  81. }
  82. errorTaskList = append(errorTaskList, msg)
  83. ackTask(TaskStreamInfo{
  84. Ctx: ctx,
  85. StreamName: streamName,
  86. GroupName: groupName,
  87. ID: msg.ID,
  88. }, true, fmt.Errorf("任务超过最大重试次数。"))
  89. continue
  90. }
  91. zap.L().Warn("taskQueue任务重试。", zap.String("ID", msg.ID), zap.Int64("count", pendingRes.RetryCount))
  92. runningID = msg.ID
  93. handleTaskQueueMsg(msg)
  94. }
  95. continue
  96. }
  97. logger.Std.Println("XReadGroup error:", err)
  98. time.Sleep(time.Second)
  99. continue
  100. }
  101. for _, stream := range res {
  102. for _, message := range stream.Messages {
  103. runningID = message.ID
  104. handleTaskQueueMsg(message)
  105. }
  106. }
  107. }
  108. }
  109. // 处理单条消息
  110. func handleTaskQueueMsg(message redis.XMessage) {
  111. id := message.ID
  112. rawPayload := message.Values["payload"].(string)
  113. var rawLockKey *string
  114. var rawLockToken *string
  115. if val, ok := message.Values["uniqueLockKey"]; ok {
  116. if str, ok := val.(string); ok {
  117. rawLockKey = &str
  118. }
  119. }
  120. if val, ok := message.Values["uniqueLockToken"]; ok {
  121. if str, ok := val.(string); ok {
  122. rawLockToken = &str
  123. }
  124. }
  125. defer func() {
  126. if rawLockKey != nil && rawLockToken != nil {
  127. // 释放唯一任务锁
  128. FinishUniqueTask(*rawLockKey, *rawLockToken)
  129. }
  130. }()
  131. // 先反序列化成 map 确定 type
  132. var base TaskPayloadBase
  133. if err := json.Unmarshal([]byte(rawPayload), &base); err != nil {
  134. ackTask(TaskStreamInfo{
  135. Ctx: ctx,
  136. StreamName: streamName,
  137. GroupName: groupName,
  138. ID: id,
  139. }, false, fmt.Errorf("unmarshal base error - %s", err.Error()))
  140. return
  141. }
  142. switch base.Type {
  143. case TaskTypeSiteAnalysis: // 站点分析任务
  144. var payload TaskPayload[domain.TaskSiteAnalysis]
  145. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  146. ackTask(TaskStreamInfo{
  147. Ctx: ctx,
  148. StreamName: streamName,
  149. GroupName: groupName,
  150. ID: id,
  151. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  152. return
  153. }
  154. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  155. if err := handleTaskSiteAnalysis(ctx, payload.Payload); err != nil {
  156. ackTask(TaskStreamInfo{
  157. Ctx: ctx,
  158. StreamName: streamName,
  159. GroupName: groupName,
  160. ID: id,
  161. }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
  162. return
  163. }
  164. case TaskTypeShutdownStatistics: // Shutdown统计任务
  165. var payload TaskPayload[domain.TaskShutdownStatistics]
  166. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  167. ackTask(TaskStreamInfo{
  168. Ctx: ctx,
  169. StreamName: streamName,
  170. GroupName: groupName,
  171. ID: id,
  172. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  173. return
  174. }
  175. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  176. if err := handleTaskShutdownStatistics(ctx, payload.Payload); err != nil {
  177. ackTask(TaskStreamInfo{
  178. Ctx: ctx,
  179. StreamName: streamName,
  180. GroupName: groupName,
  181. ID: id,
  182. }, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
  183. return
  184. }
  185. case TaskTypeBatchDownload: // 批量数据下载任务
  186. var payload TaskPayload[domain.TaskBatchDownload]
  187. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  188. ackTask(TaskStreamInfo{
  189. Ctx: ctx,
  190. StreamName: streamName,
  191. GroupName: groupName,
  192. ID: id,
  193. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  194. return
  195. }
  196. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  197. if err := handleTaskBatchDownload(ctx, payload.Payload); err != nil {
  198. ackTask(TaskStreamInfo{
  199. Ctx: ctx,
  200. StreamName: streamName,
  201. GroupName: groupName,
  202. ID: id,
  203. }, false, fmt.Errorf("handle task BatchDownload error - %s", err.Error()))
  204. return
  205. }
  206. case TaskTypeSiteInfoUpdate:
  207. var payload TaskPayload[domain.TaskSiteInfoUpdate]
  208. if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
  209. ackTask(TaskStreamInfo{
  210. Ctx: ctx,
  211. StreamName: streamName,
  212. GroupName: groupName,
  213. ID: id,
  214. }, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
  215. return
  216. }
  217. logger.Std.Printf("Processing task %s: %+v\n", id, payload)
  218. if err := handleTaskSiteInfoUpdate(ctx, payload.Payload); err != nil {
  219. ackTask(TaskStreamInfo{
  220. Ctx: ctx,
  221. StreamName: streamName,
  222. GroupName: groupName,
  223. ID: id,
  224. }, false, fmt.Errorf("handle task SiteInfoUpdate error - %s", err.Error()))
  225. return
  226. }
  227. default:
  228. logger.Std.Println("Unknown task type:", base.Type)
  229. }
  230. // 任务完成
  231. ackTask(TaskStreamInfo{
  232. Ctx: ctx,
  233. StreamName: streamName,
  234. GroupName: groupName,
  235. ID: id,
  236. }, true, nil)
  237. }
  238. func ackTask(streamInfo TaskStreamInfo, success bool, err error) {
  239. if success {
  240. // 确认任务完成
  241. if err := cache.XAckDel(streamInfo.Ctx, streamInfo.StreamName, streamInfo.GroupName, streamInfo.ID); err != nil {
  242. logger.Std.Println("XAck error:", err)
  243. }
  244. } else {
  245. if !slices.Contains(failedTaskList, streamInfo.ID) {
  246. failedTaskList = append(failedTaskList, streamInfo.ID)
  247. }
  248. // 任务失败
  249. logger.Std.Println("Task queue execution failed, error:", err)
  250. }
  251. if err != nil {
  252. zap.L().Error("taskQueue ackTask error.", zap.String("err", err.Error()))
  253. }
  254. }
  255. func EnqueueTask[T any](payload TaskPayload[T]) error {
  256. data, err := json.Marshal(payload)
  257. if err != nil {
  258. return err
  259. }
  260. if _, err := cache.XAdd(ctx, streamName, map[string]interface{}{"payload": string(data)}); err != nil {
  261. return err
  262. }
  263. return nil
  264. }
  265. func EnqueueUniqueTask[T any](payload TaskPayload[T]) error {
  266. data, err := json.Marshal(payload)
  267. if err != nil {
  268. return err
  269. }
  270. key := uniqueTaskKeyPrefix + payload.Type
  271. lock, ok, err := cache.TryLock(ctx, key, 2*time.Hour)
  272. if err != nil {
  273. return err
  274. }
  275. if !ok {
  276. return fmt.Errorf("task type already exists: %s", payload.Type)
  277. }
  278. if _, err := cache.XAdd(ctx, streamName, map[string]interface{}{"payload": string(data), "uniqueLockKey": lock.GetKey(), "uniqueLockToken": lock.GetToken()}); err != nil {
  279. lock.Release(ctx)
  280. return err
  281. }
  282. return nil
  283. }
  284. func FinishUniqueTask(key string, token string) {
  285. cache.LockRelease(ctx, key, token)
  286. }
  287. func GetTaskInfoList() ([]dto.BackgroundTaskItem, error) {
  288. c, cancel := context.WithTimeout(ctx, 15*time.Second)
  289. defer cancel()
  290. allMsg, err := cache.XRangeN(c, streamName, "-", "+", 1000) // 查询所有信息,限制1000条
  291. if err != nil {
  292. zap.L().Error("XRangeN查询失败", zap.String("err", err.Error()))
  293. return nil, err
  294. }
  295. res := []dto.BackgroundTaskItem{}
  296. for _, msg := range append(errorTaskList, allMsg...) {
  297. // 判断任务状态
  298. taskStatus := "pending" // 默认为pending
  299. if msg.ID == runningID {
  300. taskStatus = "running"
  301. } else if slices.Contains(failedTaskList, msg.ID) {
  302. taskStatus = "failed"
  303. }
  304. // 获取任务重试次数
  305. var retryCount int64
  306. pendingRes, err := cache.XPendingExtOne(ctx, streamName, groupName, retryConsumerID, msg.ID)
  307. if err != nil {
  308. zap.L().Error("XPendingExtOne查询失败", zap.String("err", err.Error()))
  309. return nil, err
  310. } else if pendingRes == nil {
  311. retryCount = 0
  312. } else {
  313. retryCount = pendingRes.RetryCount
  314. }
  315. // 获取任务参数
  316. payload, ok := msg.Values["payload"].(string)
  317. if !ok {
  318. payload = fmt.Sprint(msg.Values["taskId"])
  319. }
  320. item := dto.BackgroundTaskItem{
  321. Id: msg.ID,
  322. Status: taskStatus,
  323. Retry: retryCount,
  324. Payload: payload,
  325. }
  326. res = append(res, item)
  327. }
  328. return res, nil
  329. }