github.com/blend/go-sdk@v1.20220411.3/async/queue.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package async 9 10 import ( 11 "context" 12 "runtime" 13 "sync" 14 "time" 15 16 "github.com/blend/go-sdk/ex" 17 ) 18 19 // NewQueue returns a new parallel queue. 20 func NewQueue(action WorkAction, options ...QueueOption) *Queue { 21 q := Queue{ 22 Latch: NewLatch(), 23 Action: action, 24 Context: context.Background(), 25 MaxWork: DefaultQueueMaxWork, 26 Parallelism: runtime.NumCPU(), 27 ShutdownGracePeriod: DefaultShutdownGracePeriod, 28 } 29 for _, option := range options { 30 option(&q) 31 } 32 return &q 33 } 34 35 // QueueOption is an option for the queue worker. 36 type QueueOption func(*Queue) 37 38 // OptQueueParallelism sets the queue worker parallelism. 39 func OptQueueParallelism(parallelism int) QueueOption { 40 return func(q *Queue) { 41 q.Parallelism = parallelism 42 } 43 } 44 45 // OptQueueMaxWork sets the queue worker max work. 46 func OptQueueMaxWork(maxWork int) QueueOption { 47 return func(q *Queue) { 48 q.MaxWork = maxWork 49 } 50 } 51 52 // OptQueueErrors sets the queue worker start error channel. 53 func OptQueueErrors(errors chan error) QueueOption { 54 return func(q *Queue) { 55 q.Errors = errors 56 } 57 } 58 59 // OptQueueContext sets the queue worker context. 60 func OptQueueContext(ctx context.Context) QueueOption { 61 return func(q *Queue) { 62 q.Context = ctx 63 } 64 } 65 66 // Queue is a queue with multiple workers. 67 type Queue struct { 68 *Latch 69 70 Action WorkAction 71 Context context.Context 72 Errors chan error 73 Parallelism int 74 MaxWork int 75 ShutdownGracePeriod time.Duration 76 77 // these will typically be set by Start 78 AvailableWorkers chan *Worker 79 Workers []*Worker 80 Work chan interface{} 81 } 82 83 // Background returns a background context. 84 func (q *Queue) Background() context.Context { 85 if q.Context != nil { 86 return q.Context 87 } 88 return context.Background() 89 } 90 91 // Enqueue adds an item to the work queue. 92 func (q *Queue) Enqueue(obj interface{}) { 93 q.Work <- obj 94 } 95 96 // Start starts the queue and its workers. 97 // This call blocks. 98 func (q *Queue) Start() error { 99 if !q.Latch.CanStart() { 100 return ex.New(ErrCannotStart) 101 } 102 q.Latch.Starting() 103 104 // create channel(s) 105 q.Work = make(chan interface{}, q.MaxWork) 106 q.AvailableWorkers = make(chan *Worker, q.Parallelism) 107 q.Workers = make([]*Worker, q.Parallelism) 108 109 for x := 0; x < q.Parallelism; x++ { 110 worker := NewWorker(q.Action) 111 worker.Context = q.Context 112 worker.Errors = q.Errors 113 worker.Finalizer = q.ReturnWorker 114 115 // start the worker on its own goroutine 116 go func() { _ = worker.Start() }() 117 <-worker.NotifyStarted() 118 q.AvailableWorkers <- worker 119 q.Workers[x] = worker 120 } 121 q.Dispatch() 122 return nil 123 } 124 125 // Dispatch processes work items in a loop. 126 func (q *Queue) Dispatch() { 127 q.Latch.Started() 128 defer q.Latch.Stopped() 129 130 var workItem interface{} 131 var worker *Worker 132 var stopping <-chan struct{} 133 for { 134 stopping = q.Latch.NotifyStopping() 135 select { 136 case <-stopping: 137 return 138 case <-q.Background().Done(): 139 return 140 default: 141 } 142 143 select { 144 case <-stopping: 145 return 146 case <-q.Background().Done(): 147 return 148 case workItem = <-q.Work: 149 select { 150 case <-stopping: 151 q.Work <- workItem 152 return 153 case <-q.Background().Done(): 154 q.Work <- workItem 155 return 156 case worker = <-q.AvailableWorkers: 157 worker.Enqueue(workItem) 158 } 159 } 160 } 161 } 162 163 // Stop stops the queue and processes any remaining items. 164 func (q *Queue) Stop() error { 165 if !q.Latch.CanStop() { 166 return ex.New(ErrCannotStop) 167 } 168 q.Latch.WaitStopped() // wait for the dispatch loop to exit 169 defer q.Latch.Reset() // reset the latch in case we have to start again 170 171 timeoutContext, cancel := context.WithTimeout(q.Background(), q.ShutdownGracePeriod) 172 defer cancel() 173 174 if remainingWork := len(q.Work); remainingWork > 0 { 175 for x := 0; x < remainingWork; x++ { 176 // check the timeout first 177 select { 178 case <-timeoutContext.Done(): 179 return nil 180 default: 181 } 182 183 select { 184 case <-timeoutContext.Done(): 185 return nil 186 case workItem := <-q.Work: 187 select { 188 case <-timeoutContext.Done(): 189 return nil 190 case worker := <-q.AvailableWorkers: 191 worker.Work <- workItem 192 } 193 } 194 } 195 } 196 197 workersStopped := make(chan struct{}) 198 go func() { 199 defer close(workersStopped) 200 wg := sync.WaitGroup{} 201 wg.Add(len(q.Workers)) 202 for _, worker := range q.Workers { 203 go func(w *Worker) { 204 defer wg.Done() 205 w.StopContext(timeoutContext) 206 }(worker) 207 } 208 wg.Wait() 209 }() 210 211 select { 212 case <-timeoutContext.Done(): 213 return nil 214 case <-workersStopped: 215 return nil 216 } 217 } 218 219 // Close stops the queue. 220 // Any work left in the queue will be discarded. 221 func (q *Queue) Close() error { 222 q.Latch.WaitStopped() 223 q.Latch.Reset() 224 return nil 225 } 226 227 // ReturnWorker returns a given worker to the worker queue. 228 func (q *Queue) ReturnWorker(ctx context.Context, worker *Worker) error { 229 q.AvailableWorkers <- worker 230 return nil 231 }