github.com/blend/go-sdk@v1.20240719.1/async/worker.go (about) 1 /* 2 3 Copyright (c) 2024 - 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 13 "github.com/blend/go-sdk/ex" 14 ) 15 16 // NewWorker creates a new worker. 17 func NewWorker(action WorkAction) *Worker { 18 return &Worker{ 19 Latch: NewLatch(), 20 Context: context.Background(), 21 Action: action, 22 Work: make(chan interface{}), 23 } 24 } 25 26 // Worker is a worker that is pushed work over a channel. 27 // It is used by other work distribution types (i.e. queue and batch) 28 // but can also be used independently. 29 type Worker struct { 30 *Latch 31 32 Context context.Context 33 Action WorkAction 34 Finalizer WorkerFinalizer 35 36 SkipRecoverPanics bool 37 Errors chan error 38 Work chan interface{} 39 } 40 41 // Background returns the queue worker background context. 42 func (w *Worker) Background() context.Context { 43 if w.Context != nil { 44 return w.Context 45 } 46 return context.Background() 47 } 48 49 // NotifyStarted returns the underlying latch signal. 50 func (w *Worker) NotifyStarted() <-chan struct{} { 51 return w.Latch.NotifyStarted() 52 } 53 54 // NotifyStopped returns the underlying latch signal. 55 func (w *Worker) NotifyStopped() <-chan struct{} { 56 return w.Latch.NotifyStopped() 57 } 58 59 // Enqueue adds an item to the work queue. 60 func (w *Worker) Enqueue(obj interface{}) { 61 w.Work <- obj 62 } 63 64 // Start starts the worker with a given context. 65 func (w *Worker) Start() error { 66 if !w.Latch.CanStart() { 67 return ex.New(ErrCannotStart) 68 } 69 w.Latch.Starting() 70 w.Dispatch() 71 return nil 72 } 73 74 // Dispatch starts the listen loop for work. 75 func (w *Worker) Dispatch() { 76 w.Latch.Started() 77 defer w.Latch.Stopped() 78 79 var workItem interface{} 80 var stopping <-chan struct{} 81 for { 82 stopping = w.Latch.NotifyStopping() 83 select { 84 case <-stopping: 85 return 86 case <-w.Background().Done(): 87 return 88 default: 89 } 90 91 // block on work or stopping 92 select { 93 case workItem = <-w.Work: 94 w.Execute(w.Background(), workItem) 95 case <-stopping: 96 return 97 case <-w.Background().Done(): 98 return 99 } 100 } 101 } 102 103 // Execute invokes the action and recovers panics. 104 func (w *Worker) Execute(ctx context.Context, workItem interface{}) { 105 defer func() { 106 if !w.SkipRecoverPanics { 107 if r := recover(); r != nil { 108 w.HandleError(ex.New(r)) 109 } 110 } 111 if w.Finalizer != nil { 112 w.HandleError(w.Finalizer(ctx, w)) 113 } 114 }() 115 if w.Action != nil { 116 w.HandleError(w.Action(ctx, workItem)) 117 } 118 } 119 120 // Stop stops the worker. 121 // 122 // If there is an item left in the work channel 123 // it will be processed synchronously. 124 func (w *Worker) Stop() error { 125 if !w.Latch.CanStop() { 126 return ex.New(ErrCannotStop) 127 } 128 w.Latch.WaitStopped() 129 w.Latch.Reset() 130 return nil 131 } 132 133 // StopContext stops the worker in a given cancellation context. 134 func (w *Worker) StopContext(ctx context.Context) { 135 stopped := make(chan struct{}) 136 go func() { 137 defer func() { 138 w.Latch.Reset() 139 close(stopped) 140 }() 141 142 w.Latch.WaitStopped() 143 if workLeft := len(w.Work); workLeft > 0 { 144 for x := 0; x < workLeft; x++ { 145 w.Execute(ctx, <-w.Work) 146 } 147 } 148 }() 149 select { 150 case <-stopped: 151 return 152 case <-ctx.Done(): 153 return 154 } 155 } 156 157 // HandleError sends a non-nil err to the error 158 // collector if one is provided. 159 func (w *Worker) HandleError(err error) { 160 if err == nil { 161 return 162 } 163 if w.Errors == nil { 164 return 165 } 166 w.Errors <- err 167 }