batch_image_worker.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/config"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  8. "go.uber.org/zap"
  9. )
  10. const (
  11. defaultBatchImageWorkerLockTTL = 5 * time.Minute
  12. defaultBatchImageWorkerLockConflictDelay = 5 * time.Second
  13. defaultBatchImageWorkerErrorRetryDelay = time.Minute
  14. defaultBatchImageWorkerRequeueDelay = 30 * time.Second
  15. defaultBatchImageWorkerDelayedPollInterval = 5 * time.Second
  16. defaultBatchImageWorkerRecoveryInterval = 5 * time.Minute
  17. defaultBatchImageWorkerStaleActiveAfter = 10 * time.Minute
  18. defaultBatchImageWorkerDelayedMoveLimit = 100
  19. defaultBatchImageWorkerRecoverLimit = 100
  20. defaultBatchImageWorkerErrorBackoff = time.Second
  21. defaultBatchImageWorkerReserveBlockTimeout = 5 * time.Second
  22. )
  23. type BatchImageProcessor interface {
  24. Process(ctx context.Context, batchID string) (BatchImageProcessResult, error)
  25. }
  26. type BatchImageProcessResult struct {
  27. RequeueAfter time.Duration
  28. Terminal bool
  29. }
  30. type BatchImageWorkerOptions struct {
  31. ReserveBlockTimeout time.Duration
  32. JobLockTTL time.Duration
  33. LockConflictDelay time.Duration
  34. DefaultRequeueDelay time.Duration
  35. ErrorRetryDelay time.Duration
  36. ErrorBackoff time.Duration
  37. DelayedPollInterval time.Duration
  38. RecoveryInterval time.Duration
  39. StaleActiveAfter time.Duration
  40. DelayedMoveLimit int
  41. RecoverLimit int
  42. }
  43. type BatchImageWorker struct {
  44. queue BatchImageQueue
  45. processor BatchImageProcessor
  46. opts BatchImageWorkerOptions
  47. }
  48. func NewBatchImageWorker(queue BatchImageQueue, processor BatchImageProcessor, opts BatchImageWorkerOptions) *BatchImageWorker {
  49. return &BatchImageWorker{
  50. queue: queue,
  51. processor: processor,
  52. opts: normalizeBatchImageWorkerOptions(opts),
  53. }
  54. }
  55. func NewBatchImageWorkerOptionsFromConfig(cfg *config.Config) BatchImageWorkerOptions {
  56. if cfg == nil {
  57. return normalizeBatchImageWorkerOptions(BatchImageWorkerOptions{})
  58. }
  59. return normalizeBatchImageWorkerOptions(BatchImageWorkerOptions{
  60. JobLockTTL: time.Duration(cfg.BatchImage.JobLockTTLSeconds) * time.Second,
  61. LockConflictDelay: time.Duration(cfg.BatchImage.LockConflictDelaySeconds) * time.Second,
  62. DefaultRequeueDelay: time.Duration(cfg.BatchImage.DefaultRequeueDelaySeconds) * time.Second,
  63. ErrorRetryDelay: time.Duration(cfg.BatchImage.ErrorRetryDelaySeconds) * time.Second,
  64. DelayedPollInterval: time.Duration(cfg.BatchImage.DelayedMoverIntervalSeconds) * time.Second,
  65. RecoveryInterval: time.Duration(cfg.BatchImage.RecoveryIntervalSeconds) * time.Second,
  66. StaleActiveAfter: time.Duration(cfg.BatchImage.StaleActiveAfterSeconds) * time.Second,
  67. DelayedMoveLimit: cfg.BatchImage.DelayedMoveLimit,
  68. RecoverLimit: cfg.BatchImage.RecoverLimit,
  69. })
  70. }
  71. func normalizeBatchImageWorkerOptions(opts BatchImageWorkerOptions) BatchImageWorkerOptions {
  72. if opts.ReserveBlockTimeout <= 0 {
  73. opts.ReserveBlockTimeout = defaultBatchImageWorkerReserveBlockTimeout
  74. }
  75. if opts.JobLockTTL <= 0 {
  76. opts.JobLockTTL = defaultBatchImageWorkerLockTTL
  77. }
  78. if opts.LockConflictDelay <= 0 {
  79. opts.LockConflictDelay = defaultBatchImageWorkerLockConflictDelay
  80. }
  81. if opts.DefaultRequeueDelay <= 0 {
  82. opts.DefaultRequeueDelay = defaultBatchImageWorkerRequeueDelay
  83. }
  84. if opts.ErrorRetryDelay <= 0 {
  85. opts.ErrorRetryDelay = defaultBatchImageWorkerErrorRetryDelay
  86. }
  87. if opts.ErrorBackoff <= 0 {
  88. opts.ErrorBackoff = defaultBatchImageWorkerErrorBackoff
  89. }
  90. if opts.DelayedPollInterval <= 0 {
  91. opts.DelayedPollInterval = defaultBatchImageWorkerDelayedPollInterval
  92. }
  93. if opts.RecoveryInterval <= 0 {
  94. opts.RecoveryInterval = defaultBatchImageWorkerRecoveryInterval
  95. }
  96. if opts.StaleActiveAfter <= 0 {
  97. opts.StaleActiveAfter = defaultBatchImageWorkerStaleActiveAfter
  98. }
  99. if opts.DelayedMoveLimit <= 0 {
  100. opts.DelayedMoveLimit = defaultBatchImageWorkerDelayedMoveLimit
  101. }
  102. if opts.RecoverLimit <= 0 {
  103. opts.RecoverLimit = defaultBatchImageWorkerRecoverLimit
  104. }
  105. return opts
  106. }
  107. func (w *BatchImageWorker) Run(ctx context.Context) {
  108. if w == nil {
  109. return
  110. }
  111. for {
  112. if err := ctx.Err(); err != nil {
  113. return
  114. }
  115. if err := w.RunOnce(ctx); err != nil && ctx.Err() == nil {
  116. sleepOrDone(ctx, w.opts.ErrorBackoff)
  117. }
  118. }
  119. }
  120. func (w *BatchImageWorker) RunOnce(ctx context.Context) error {
  121. if w == nil || w.queue == nil || w.processor == nil {
  122. return nil
  123. }
  124. reserved, err := w.queue.Reserve(ctx, w.opts.ReserveBlockTimeout)
  125. if errors.Is(err, ErrBatchImageQueueEmpty) {
  126. return nil
  127. }
  128. if err != nil {
  129. return err
  130. }
  131. lock, ok, err := w.queue.TryAcquireJobLock(ctx, reserved.BatchID, w.opts.JobLockTTL)
  132. if err != nil {
  133. if requeueErr := w.queue.RequeueAfter(ctx, reserved.BatchID, w.opts.LockConflictDelay); requeueErr != nil {
  134. return requeueErr
  135. }
  136. return err
  137. }
  138. if !ok {
  139. // 锁被其他实例持有:按冲突延迟重新入队。直接丢弃会让 job 滞留在
  140. // active zset,最早要等 StaleActiveAfter 才被恢复,造成分钟级停摆。
  141. return w.queue.RequeueAfter(ctx, reserved.BatchID, w.opts.LockConflictDelay)
  142. }
  143. defer func() {
  144. _ = lock.Release(ctx)
  145. }()
  146. // 处理期间持续心跳:刷新 active zset 时间戳防止 stale 恢复把在处理的
  147. // job 重投给其他 worker,并对支持续期的锁实现延长锁 TTL。
  148. hbStop := make(chan struct{})
  149. hbDone := make(chan struct{})
  150. go w.runJobHeartbeat(ctx, reserved.BatchID, lock, hbStop, hbDone)
  151. result, err := w.processor.Process(ctx, reserved.BatchID)
  152. close(hbStop)
  153. <-hbDone
  154. if err != nil {
  155. logger.L().Warn("batch_image.worker_process_failed",
  156. zap.String("batch_id", reserved.BatchID),
  157. zap.Error(err),
  158. )
  159. return w.queue.RequeueAfter(ctx, reserved.BatchID, w.opts.ErrorRetryDelay)
  160. }
  161. if result.Terminal {
  162. return w.queue.Ack(ctx, reserved.BatchID)
  163. }
  164. delay := result.RequeueAfter
  165. if delay <= 0 {
  166. delay = w.opts.DefaultRequeueDelay
  167. }
  168. return w.queue.RequeueAfter(ctx, reserved.BatchID, delay)
  169. }
  170. // BatchImageJobLockRefresher 是可选的锁续期能力;由具体锁实现按需提供。
  171. type BatchImageJobLockRefresher interface {
  172. Refresh(ctx context.Context, ttl time.Duration) error
  173. }
  174. func (w *BatchImageWorker) heartbeatInterval() time.Duration {
  175. interval := w.opts.JobLockTTL
  176. if w.opts.StaleActiveAfter < interval {
  177. interval = w.opts.StaleActiveAfter
  178. }
  179. interval /= 3
  180. if interval < time.Second {
  181. interval = time.Second
  182. }
  183. return interval
  184. }
  185. func (w *BatchImageWorker) runJobHeartbeat(ctx context.Context, batchID string, lock BatchImageJobLock, stop <-chan struct{}, done chan<- struct{}) {
  186. defer close(done)
  187. ticker := time.NewTicker(w.heartbeatInterval())
  188. defer ticker.Stop()
  189. for {
  190. select {
  191. case <-stop:
  192. return
  193. case <-ctx.Done():
  194. return
  195. case <-ticker.C:
  196. if err := w.queue.Heartbeat(ctx, batchID); err != nil && ctx.Err() == nil {
  197. logger.L().Warn("batch_image.worker_heartbeat_failed",
  198. zap.String("batch_id", batchID),
  199. zap.Error(err),
  200. )
  201. }
  202. if refresher, ok := lock.(BatchImageJobLockRefresher); ok {
  203. if err := refresher.Refresh(ctx, w.opts.JobLockTTL); err != nil && ctx.Err() == nil {
  204. logger.L().Warn("batch_image.worker_lock_refresh_failed",
  205. zap.String("batch_id", batchID),
  206. zap.Error(err),
  207. )
  208. }
  209. }
  210. }
  211. }
  212. }
  213. func (w *BatchImageWorker) MoveDueDelayedOnce(ctx context.Context) (int, error) {
  214. if w == nil || w.queue == nil {
  215. return 0, nil
  216. }
  217. return w.queue.MoveDueDelayedToReady(ctx, w.opts.DelayedMoveLimit)
  218. }
  219. func (w *BatchImageWorker) RunDelayedMover(ctx context.Context) {
  220. if w == nil {
  221. return
  222. }
  223. for {
  224. if err := ctx.Err(); err != nil {
  225. return
  226. }
  227. moved, _ := w.MoveDueDelayedOnce(ctx)
  228. if moved > 0 {
  229. continue
  230. }
  231. sleepOrDone(ctx, w.opts.DelayedPollInterval)
  232. }
  233. }
  234. func (w *BatchImageWorker) RecoverStaleActiveOnce(ctx context.Context) (int, error) {
  235. if w == nil || w.queue == nil {
  236. return 0, nil
  237. }
  238. return w.queue.RecoverStaleActive(ctx, w.opts.StaleActiveAfter, w.opts.RecoverLimit)
  239. }
  240. func (w *BatchImageWorker) RunStaleActiveRecovery(ctx context.Context) {
  241. if w == nil {
  242. return
  243. }
  244. for {
  245. if err := ctx.Err(); err != nil {
  246. return
  247. }
  248. _, _ = w.RecoverStaleActiveOnce(ctx)
  249. sleepOrDone(ctx, w.opts.RecoveryInterval)
  250. }
  251. }
  252. func sleepOrDone(ctx context.Context, d time.Duration) {
  253. if d <= 0 {
  254. return
  255. }
  256. timer := time.NewTimer(d)
  257. defer timer.Stop()
  258. select {
  259. case <-ctx.Done():
  260. case <-timer.C:
  261. }
  262. }