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  }