github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/workerpool/workerpool.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package workerpool 5 6 import ( 7 "strings" 8 "sync" 9 10 "github.com/juju/errors" 11 ) 12 13 // Logger defines the logging methods that the worker pool uses. 14 type Logger interface { 15 Errorf(string, ...interface{}) 16 Debugf(string, ...interface{}) 17 Tracef(string, ...interface{}) 18 } 19 20 // Task represents a unit of work which should be executed by the pool workers. 21 type Task struct { 22 // Type is a short, optional description of the task which will be 23 // included in emitted log messages. 24 Type string 25 26 // Process encapsulates the logic for processing a task. 27 Process func() error 28 } 29 30 // WorkerPool implements a fixed-size worker pool for processing tasks in 31 // parallel. If any of the tasks fails, the pool workers will be shut down, and 32 // any detected errors (different tasks may fail concurrently) will be 33 // consolidated and reported when the pool's Close() method is invoked. 34 // 35 // New tasks can be enqueued by writing to the channel returned by the 36 // pool's Queue() method while the pool's Done() method returns a channel 37 // which can be used by callers to detect when an error occurred and the 38 // pool is shutting down. 39 type WorkerPool struct { 40 logger Logger 41 42 // A channel used to signal workers that they should finish their 43 // work and exit. 44 shutdownTriggerCh chan struct{} 45 46 // The worker pool can be stopped either via a call to Shutdown() or 47 // directly by one of the workers when it encounters an error. A 48 // sync.Once primitive ensures that the shutdown trigger channel can 49 // only be closed once. 50 shutdownTrigger sync.Once 51 closeErrorChannel sync.Once 52 53 // A waitgroup for ensuring that all workers have exited. It is used 54 // both when the pool is being shutdown and also as a barrier to ensure 55 // that workers have exited before draining the error reporting channel. 56 wg sync.WaitGroup 57 58 // A buffered channel (cap equal to pool size) which workers monitor 59 // for incoming processing tasks. 60 taskQueueCh chan Task 61 62 // A buffered channel (cap equal to pool size) which workers emit 63 // any errors they encounter before taskuesting the pool to be shut 64 // down. 65 taskErrCh chan error 66 } 67 68 // NewWorkerPool returns a pool with the taskuested number of workers. Callers 69 // must ensure to call the pool's Close() method to avoid leaking goroutines. 70 func NewWorkerPool(logger Logger, size int) *WorkerPool { 71 // Size must be at least one 72 if size <= 0 { 73 size = 1 74 } 75 76 wp := &WorkerPool{ 77 logger: logger, 78 shutdownTriggerCh: make(chan struct{}), 79 taskQueueCh: make(chan Task, size), 80 taskErrCh: make(chan error, size), 81 } 82 83 wp.wg.Add(size) 84 for workerID := 0; workerID < size; workerID++ { 85 wp.logger.Tracef("worker %d: starting new worker pool", workerID) 86 go wp.taskWorker(workerID) 87 } 88 89 return wp 90 } 91 92 // Size returns the number of workers in the pool. 93 func (wp *WorkerPool) Size() int { 94 return cap(wp.taskQueueCh) 95 } 96 97 // Queue returns a channel for enqueueing processing tasks. 98 func (wp *WorkerPool) Queue() chan<- Task { 99 return wp.taskQueueCh 100 } 101 102 // Done returns a channel which is closed if the pool has detected one or more 103 // errors and is shutting down. Callers must then invoke the pool's Close method 104 // to obtain any reported errors. 105 func (wp *WorkerPool) Done() <-chan struct{} { 106 return wp.shutdownTriggerCh 107 } 108 109 // Close the pool and return any queued errors. The method signals and waits 110 // for all workers to exit before draining the worker error channel and 111 // returning a combined error (if any errors were reported) value. After a call 112 // to Shutdown, no further provision tasks will be accepted by the pool. 113 func (wp *WorkerPool) Close() error { 114 wp.triggerShutdown() 115 wp.wg.Wait() // wait for workers to exit and write any errors they encounter 116 117 // Now we can safely close and drain the error channel. 118 wp.closeErrorChannel.Do(func() { 119 close(wp.taskErrCh) 120 }) 121 122 var errList []string 123 for err := range wp.taskErrCh { 124 errList = append(errList, err.Error()) 125 } 126 127 if len(errList) == 0 { 128 return nil 129 } 130 return errors.New(strings.Join(errList, "\n")) 131 } 132 133 func (wp *WorkerPool) taskWorker(workerID int) { 134 defer wp.wg.Done() 135 for { 136 select { 137 case task := <-wp.taskQueueCh: 138 wp.logger.Debugf("worker %d: processing task %q", workerID, task.Type) 139 if err := task.Process(); err != nil { 140 wp.logger.Errorf("worker %d: shutting down pool due to error while handling a %q task: %v", workerID, task.Type, err) 141 142 // This is a buffered channel to allow every pool worker to report 143 // a single error before it exits. Consequently, this call can never 144 // block. 145 wp.taskErrCh <- err 146 wp.triggerShutdown() 147 148 return // worker cannot process any further tasks. 149 } 150 case <-wp.shutdownTriggerCh: 151 wp.logger.Tracef("worker %d: terminating as worker pool is shutting down", workerID) 152 return 153 } 154 } 155 } 156 157 // triggerShutdown notifies all workers to exit once they complete the tasks 158 // they are currently processing. Workers can only be notified once; subsequent 159 // calls to this method are no-ops. 160 func (wp *WorkerPool) triggerShutdown() { 161 wp.shutdownTrigger.Do(func() { 162 close(wp.shutdownTriggerCh) 163 }) 164 }