github.com/onflow/flow-go@v0.33.17/engine/common/worker/worker_builder.go (about) 1 package worker 2 3 import ( 4 "fmt" 5 6 "github.com/rs/zerolog" 7 8 "github.com/onflow/flow-go/engine" 9 "github.com/onflow/flow-go/module/component" 10 "github.com/onflow/flow-go/module/irrecoverable" 11 ) 12 13 // Pool is a worker pool that can be used by a higher-level component to manage a set of workers. 14 // The workers are managed by the higher-level component, but the worker pool provides the logic for 15 // submitting work to the workers and for processing the work. The worker pool is responsible for 16 // storing the work until it is processed by a worker. 17 type Pool[T any] struct { 18 // workerLogic is the logic that the worker executes. It is the responsibility of the higher-level 19 // component to add this logic to the component. The worker logic is responsible for processing 20 // the work that is submitted to the worker pool. 21 // The worker logic should only throw unexpected exceptions to its component. Sentinel errors expected during 22 // normal operations should be handled internally. 23 // The worker logic should not throw any exceptions that are not expected during normal operations. 24 // Any exceptions thrown by the worker logic will be caught by the higher-level component and will cause the 25 // component to crash. 26 // A pool may have multiple workers, but the worker logic is the same for all the workers. 27 workerLogic component.ComponentWorker 28 29 // submitLogic is the logic that the higher-level component executes to submit work to the worker pool. 30 // The submit logic is responsible for submitting the work to the worker pool. The submit logic should 31 // is responsible for storing the work until it is processed by a worker. The submit should handle 32 // any errors that occur during the submission of the work internally. 33 // The return value of the submit logic indicates whether the work was successfully submitted to the worker pool. 34 submitLogic func(event T) bool 35 } 36 37 // WorkerLogic returns a new worker logic that can be added to a component. The worker logic is responsible for 38 // processing the work that is submitted to the worker pool. 39 // A pool may have multiple workers, but the worker logic is the same for all the workers. 40 // Workers are managed by the higher-level component, through component.AddWorker. 41 func (p *Pool[T]) WorkerLogic() component.ComponentWorker { 42 return p.workerLogic 43 } 44 45 // Submit submits work to the worker pool. The submit logic is responsible for submitting the work to the worker pool. 46 func (p *Pool[T]) Submit(event T) bool { 47 return p.submitLogic(event) 48 } 49 50 // PoolBuilder is an auxiliary builder for constructing workers with a common inbound queue, 51 // where the workers are managed by a higher-level component. 52 // 53 // The message store as well as the processing function are specified by the caller. 54 // WorkerPoolBuilder does not add any concurrency handling. 55 // It is the callers responsibility to make sure that the number of workers concurrently accessing `processingFunc` 56 // is compatible with its implementation. 57 type PoolBuilder[T any] struct { 58 logger zerolog.Logger 59 store engine.MessageStore // temporarily store inbound events till they are processed. 60 notifier engine.Notifier 61 62 // processingFunc is the function for processing the input tasks. It should only return unexpected 63 // exceptions. Sentinel errors expected during normal operations should be handled internally. 64 processingFunc func(T) error 65 } 66 67 // NewWorkerPoolBuilder creates a new PoolBuilder, which is an auxiliary builder 68 // for constructing workers with a common inbound queue. 69 // Arguments: 70 // -`processingFunc`: the function for processing the input tasks. 71 // -`store`: temporarily stores inbound events until they are processed. 72 // Returns: 73 // The function returns a `PoolBuilder` instance. 74 func NewWorkerPoolBuilder[T any]( 75 logger zerolog.Logger, 76 store engine.MessageStore, 77 processingFunc func(input T) error, 78 ) *PoolBuilder[T] { 79 return &PoolBuilder[T]{ 80 logger: logger.With().Str("component", "worker-pool").Logger(), 81 store: store, 82 notifier: engine.NewNotifier(), 83 processingFunc: processingFunc, 84 } 85 } 86 87 // Build builds a new worker pool. The worker pool is responsible for storing the work until it is processed by a worker. 88 func (b *PoolBuilder[T]) Build() *Pool[T] { 89 return &Pool[T]{ 90 workerLogic: b.workerLogic(), 91 submitLogic: b.submitLogic(), 92 } 93 } 94 95 // workerLogic returns an abstract function for processing work from the message store. 96 // The worker logic picks up work from the message store and processes it. 97 // The worker logic should only throw unexpected exceptions to its component. Sentinel errors expected during 98 // normal operations should be handled internally. 99 // The worker logic should not throw any exceptions that are not expected during normal operations. 100 // Any exceptions thrown by the worker logic will be caught by the higher-level component and will cause the 101 // component to crash. 102 func (b *PoolBuilder[T]) workerLogic() component.ComponentWorker { 103 notifier := b.notifier.Channel() 104 processingFunc := b.processingFunc 105 store := b.store 106 107 return func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 108 ready() // wait for ready signal 109 110 for { 111 select { 112 case <-ctx.Done(): 113 b.logger.Trace().Msg("worker logic shutting down") 114 return 115 case <-notifier: 116 for { // on single notification, commence processing items in store until none left 117 select { 118 case <-ctx.Done(): 119 b.logger.Trace().Msg("worker logic shutting down") 120 return 121 default: 122 } 123 124 msg, ok := store.Get() 125 if !ok { 126 b.logger.Trace().Msg("store is empty, waiting for next notification") 127 break // store is empty; go back to outer for loop 128 } 129 b.logger.Trace().Msg("processing queued work item") 130 err := processingFunc(msg.Payload.(T)) 131 b.logger.Trace().Msg("finished processing queued work item") 132 if err != nil { 133 ctx.Throw(fmt.Errorf("unexpected error processing queued work item: %w", err)) 134 return 135 } 136 } 137 } 138 } 139 } 140 } 141 142 // submitLogic returns an abstract function for submitting work to the message store. 143 // The submit logic is responsible for submitting the work to the worker pool. The submit logic should 144 // is responsible for storing the work until it is processed by a worker. The submit should handle 145 // any errors that occur during the submission of the work internally. 146 func (b *PoolBuilder[T]) submitLogic() func(event T) bool { 147 store := b.store 148 149 return func(event T) bool { 150 ok := store.Put(&engine.Message{ 151 Payload: event, 152 }) 153 if !ok { 154 return false 155 } 156 b.notifier.Notify() 157 return true 158 } 159 }