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