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  }