go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/worker.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package async 9 10 import ( 11 "context" 12 "fmt" 13 ) 14 15 // NewWorker creates a new worker. 16 func NewWorker[T any](action WorkAction[T]) *Worker[T] { 17 return &Worker[T]{ 18 Latch: NewLatch(), 19 Context: context.Background(), 20 Action: action, 21 Work: make(chan T), 22 } 23 } 24 25 // WorkAction is an action handler for a queue. 26 type WorkAction[T any] func(context.Context, T) error 27 28 // WorkerFinalizer is an action handler for a queue. 29 type WorkerFinalizer[T any] func(context.Context, *Worker[T]) error 30 31 // Worker is a worker that is pushed work over a channel. 32 // It is used by other work distribution types (i.e. queue and batch) 33 // but can also be used independently. 34 type Worker[T any] struct { 35 *Latch 36 37 Context context.Context 38 Action WorkAction[T] 39 Finalizer WorkerFinalizer[T] 40 41 SkipRecover bool 42 Errors chan error 43 Work chan T 44 } 45 46 // Background returns the queue worker background context. 47 func (w *Worker[T]) Background() context.Context { 48 if w.Context != nil { 49 return w.Context 50 } 51 return context.Background() 52 } 53 54 // NotifyStarted returns the underlying latch signal. 55 func (w *Worker[T]) NotifyStarted() <-chan struct{} { 56 return w.Latch.NotifyStarted() 57 } 58 59 // NotifyStopped returns the underlying latch signal. 60 func (w *Worker[T]) NotifyStopped() <-chan struct{} { 61 return w.Latch.NotifyStarted() 62 } 63 64 // Start starts the worker with a given context. 65 func (w *Worker[T]) Start() error { 66 if !w.Latch.CanStart() { 67 return 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[T]) Dispatch() { 76 w.Latch.Started() 77 defer w.Latch.Stopped() 78 79 var workItem T 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[T]) Execute(ctx context.Context, workItem T) { 105 defer func() { 106 if !w.SkipRecover { 107 if r := recover(); r != nil { 108 w.HandlePanic(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[T]) Stop() error { 125 if !w.Latch.CanStop() { 126 return 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[T]) 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[T]) HandlePanic(r interface{}) { 160 if r == nil { 161 return 162 } 163 if w.Errors == nil { 164 return 165 } 166 w.Errors <- fmt.Errorf("%v", r) 167 } 168 169 // HandleError sends a non-nil err to the error 170 // collector if one is provided. 171 func (w *Worker[T]) HandleError(err error) { 172 if err == nil { 173 return 174 } 175 if w.Errors == nil { 176 return 177 } 178 w.Errors <- err 179 }