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  }