code.gitea.io/gitea@v1.22.3/modules/queue/workerqueue.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package queue 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "code.gitea.io/gitea/modules/json" 14 "code.gitea.io/gitea/modules/log" 15 "code.gitea.io/gitea/modules/process" 16 "code.gitea.io/gitea/modules/setting" 17 ) 18 19 // WorkerPoolQueue is a queue that uses a pool of workers to process items 20 // It can use different underlying (base) queue types 21 type WorkerPoolQueue[T any] struct { 22 ctxRun context.Context 23 ctxRunCancel context.CancelFunc 24 25 shutdownDone chan struct{} 26 shutdownTimeout atomic.Int64 // in case some buggy handlers (workers) would hang forever, "shutdown" should finish in predictable time 27 28 origHandler HandlerFuncT[T] 29 safeHandler HandlerFuncT[T] 30 31 baseQueueType string 32 baseConfig *BaseConfig 33 baseQueue baseQueue 34 35 batchChan chan []T 36 flushChan chan flushType 37 38 batchLength int 39 workerNum int 40 workerMaxNum int 41 workerActiveNum int 42 workerNumMu sync.Mutex 43 } 44 45 type flushType chan struct{} 46 47 var _ ManagedWorkerPoolQueue = (*WorkerPoolQueue[any])(nil) 48 49 func (q *WorkerPoolQueue[T]) GetName() string { 50 return q.baseConfig.ManagedName 51 } 52 53 func (q *WorkerPoolQueue[T]) GetType() string { 54 return q.baseQueueType 55 } 56 57 func (q *WorkerPoolQueue[T]) GetItemTypeName() string { 58 var t T 59 return fmt.Sprintf("%T", t) 60 } 61 62 func (q *WorkerPoolQueue[T]) GetWorkerNumber() int { 63 q.workerNumMu.Lock() 64 defer q.workerNumMu.Unlock() 65 return q.workerNum 66 } 67 68 func (q *WorkerPoolQueue[T]) GetWorkerActiveNumber() int { 69 q.workerNumMu.Lock() 70 defer q.workerNumMu.Unlock() 71 return q.workerActiveNum 72 } 73 74 func (q *WorkerPoolQueue[T]) GetWorkerMaxNumber() int { 75 q.workerNumMu.Lock() 76 defer q.workerNumMu.Unlock() 77 return q.workerMaxNum 78 } 79 80 func (q *WorkerPoolQueue[T]) SetWorkerMaxNumber(num int) { 81 q.workerNumMu.Lock() 82 defer q.workerNumMu.Unlock() 83 q.workerMaxNum = num 84 } 85 86 func (q *WorkerPoolQueue[T]) GetQueueItemNumber() int { 87 cnt, err := q.baseQueue.Len(q.ctxRun) 88 if err != nil { 89 log.Error("Failed to get number of items in queue %q: %v", q.GetName(), err) 90 } 91 return cnt 92 } 93 94 func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time.Duration) (err error) { 95 if q.isBaseQueueDummy() { 96 return nil 97 } 98 99 log.Debug("Try to flush queue %q with timeout %v", q.GetName(), timeout) 100 defer log.Debug("Finish flushing queue %q, err: %v", q.GetName(), err) 101 102 var after <-chan time.Time 103 after = infiniteTimerC 104 if timeout > 0 { 105 after = time.After(timeout) 106 } 107 c := make(flushType) 108 109 // send flush request 110 // if it blocks, it means that there is a flush in progress or the queue hasn't been started yet 111 select { 112 case q.flushChan <- c: 113 case <-ctx.Done(): 114 return ctx.Err() 115 case <-q.ctxRun.Done(): 116 return q.ctxRun.Err() 117 case <-after: 118 return context.DeadlineExceeded 119 } 120 121 // wait for flush to finish 122 select { 123 case <-c: 124 return nil 125 case <-ctx.Done(): 126 return ctx.Err() 127 case <-q.ctxRun.Done(): 128 return q.ctxRun.Err() 129 case <-after: 130 return context.DeadlineExceeded 131 } 132 } 133 134 // RemoveAllItems removes all items in the baes queue 135 func (q *WorkerPoolQueue[T]) RemoveAllItems(ctx context.Context) error { 136 return q.baseQueue.RemoveAll(ctx) 137 } 138 139 func (q *WorkerPoolQueue[T]) marshal(data T) []byte { 140 bs, err := json.Marshal(data) 141 if err != nil { 142 log.Error("Failed to marshal item for queue %q: %v", q.GetName(), err) 143 return nil 144 } 145 return bs 146 } 147 148 func (q *WorkerPoolQueue[T]) unmarshal(data []byte) (t T, ok bool) { 149 if err := json.Unmarshal(data, &t); err != nil { 150 log.Error("Failed to unmarshal item from queue %q: %v", q.GetName(), err) 151 return t, false 152 } 153 return t, true 154 } 155 156 func (q *WorkerPoolQueue[T]) isBaseQueueDummy() bool { 157 _, isDummy := q.baseQueue.(*baseDummy) 158 return isDummy 159 } 160 161 // Push adds an item to the queue, it may block for a while and then returns an error if the queue is full 162 func (q *WorkerPoolQueue[T]) Push(data T) error { 163 if q.isBaseQueueDummy() && q.safeHandler != nil { 164 // FIXME: the "immediate" queue is only for testing, but it really causes problems because its behavior is different from a real queue. 165 // Even if tests pass, it doesn't mean that there is no bug in code. 166 if data, ok := q.unmarshal(q.marshal(data)); ok { 167 q.safeHandler(data) 168 } 169 } 170 return q.baseQueue.PushItem(q.ctxRun, q.marshal(data)) 171 } 172 173 // Has only works for unique queues. Keep in mind that this check may not be reliable (due to lacking of proper transaction support) 174 // There could be a small chance that duplicate items appear in the queue 175 func (q *WorkerPoolQueue[T]) Has(data T) (bool, error) { 176 return q.baseQueue.HasItem(q.ctxRun, q.marshal(data)) 177 } 178 179 func (q *WorkerPoolQueue[T]) Run() { 180 q.doRun() 181 } 182 183 func (q *WorkerPoolQueue[T]) Cancel() { 184 q.ctxRunCancel() 185 } 186 187 // ShutdownWait shuts down the queue, waits for all workers to finish their jobs, and pushes the unhandled items back to the base queue 188 // It waits for all workers (handlers) to finish their jobs, in case some buggy handlers would hang forever, a reasonable timeout is needed 189 func (q *WorkerPoolQueue[T]) ShutdownWait(timeout time.Duration) { 190 q.shutdownTimeout.Store(int64(timeout)) 191 q.ctxRunCancel() 192 <-q.shutdownDone 193 } 194 195 func getNewQueueFn(t string) (string, func(cfg *BaseConfig, unique bool) (baseQueue, error)) { 196 switch t { 197 case "dummy", "immediate": 198 return t, newBaseDummy 199 case "channel": 200 return t, newBaseChannelGeneric 201 case "redis": 202 return t, newBaseRedisGeneric 203 default: // level(leveldb,levelqueue,persistable-channel) 204 return "level", newBaseLevelQueueGeneric 205 } 206 } 207 208 func newWorkerPoolQueueForTest[T any](name string, queueSetting setting.QueueSettings, handler HandlerFuncT[T], unique bool) (*WorkerPoolQueue[T], error) { 209 return NewWorkerPoolQueueWithContext(context.Background(), name, queueSetting, handler, unique) 210 } 211 212 func NewWorkerPoolQueueWithContext[T any](ctx context.Context, name string, queueSetting setting.QueueSettings, handler HandlerFuncT[T], unique bool) (*WorkerPoolQueue[T], error) { 213 if handler == nil { 214 log.Debug("Use dummy queue for %q because handler is nil and caller doesn't want to process the queue items", name) 215 queueSetting.Type = "dummy" 216 } 217 218 var w WorkerPoolQueue[T] 219 var err error 220 queueType, newQueueFn := getNewQueueFn(queueSetting.Type) 221 w.baseQueueType = queueType 222 w.baseConfig = toBaseConfig(name, queueSetting) 223 w.baseQueue, err = newQueueFn(w.baseConfig, unique) 224 if err != nil { 225 return nil, err 226 } 227 log.Trace("Created queue %q of type %q", name, queueType) 228 229 w.ctxRun, _, w.ctxRunCancel = process.GetManager().AddTypedContext(ctx, "Queue: "+w.GetName(), process.SystemProcessType, false) 230 w.batchChan = make(chan []T) 231 w.flushChan = make(chan flushType) 232 w.shutdownDone = make(chan struct{}) 233 w.shutdownTimeout.Store(int64(shutdownDefaultTimeout)) 234 w.workerMaxNum = queueSetting.MaxWorkers 235 w.batchLength = queueSetting.BatchLength 236 237 w.origHandler = handler 238 w.safeHandler = func(t ...T) (unhandled []T) { 239 defer func() { 240 err := recover() 241 if err != nil { 242 log.Error("Recovered from panic in queue %q handler: %v\n%s", name, err, log.Stack(2)) 243 } 244 }() 245 if w.origHandler != nil { 246 return w.origHandler(t...) 247 } 248 return nil 249 } 250 251 return &w, nil 252 }