github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/worker/worker_builder_test.go (about)

     1  package worker_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/onflow/flow-go/engine/common/worker"
    13  	"github.com/onflow/flow-go/module/component"
    14  	"github.com/onflow/flow-go/module/irrecoverable"
    15  	"github.com/onflow/flow-go/module/mempool/queue"
    16  	"github.com/onflow/flow-go/module/metrics"
    17  	"github.com/onflow/flow-go/utils/unittest"
    18  )
    19  
    20  // TestWorkerPool_SingleEvent_SingleWorker tests the worker pool with a single worker and a single event.
    21  // It submits an event to the worker pool and checks if the event is processed by the worker.
    22  func TestWorkerPool_SingleEvent_SingleWorker(t *testing.T) {
    23  	event := "test-event"
    24  
    25  	q := queue.NewHeroStore(10, unittest.Logger(), metrics.NewNoopCollector())
    26  	processed := make(chan struct{})
    27  
    28  	pool := worker.NewWorkerPoolBuilder[string](
    29  		unittest.Logger(),
    30  		q,
    31  		func(input string) error {
    32  			require.Equal(t, event, event)
    33  			close(processed)
    34  
    35  			return nil
    36  		}).Build()
    37  
    38  	cancelCtx, cancel := context.WithCancel(context.Background())
    39  	defer cancel()
    40  	ctx, _ := irrecoverable.WithSignaler(cancelCtx)
    41  	cm := component.NewComponentManagerBuilder().
    42  		AddWorker(pool.WorkerLogic()).
    43  		Build()
    44  	cm.Start(ctx)
    45  
    46  	unittest.RequireCloseBefore(t, cm.Ready(), 100*time.Millisecond, "could not start worker")
    47  
    48  	require.True(t, pool.Submit(event))
    49  
    50  	unittest.RequireCloseBefore(t, processed, 100*time.Millisecond, "event not processed")
    51  	cancel()
    52  	unittest.RequireCloseBefore(t, cm.Done(), 100*time.Millisecond, "could not stop worker")
    53  }
    54  
    55  // TestWorkerBuilder_UnhappyPaths verifies that the WorkerBuilder can handle queue overflows, duplicate submissions.
    56  func TestWorkerBuilder_UnhappyPaths(t *testing.T) {
    57  	size := 5
    58  
    59  	q := queue.NewHeroStore(uint32(size), unittest.Logger(), metrics.NewNoopCollector())
    60  
    61  	blockingChannel := make(chan struct{})
    62  	firstEventArrived := make(chan struct{})
    63  
    64  	pool := worker.NewWorkerPoolBuilder[string](
    65  		unittest.Logger(),
    66  		q,
    67  		func(input string) error {
    68  			close(firstEventArrived)
    69  			// we block the consumer to make sure that the queue is eventually full.
    70  			<-blockingChannel
    71  
    72  			return nil
    73  		}).Build()
    74  
    75  	cancelCtx, cancel := context.WithCancel(context.Background())
    76  	defer cancel()
    77  	ctx, _ := irrecoverable.WithSignaler(cancelCtx)
    78  	cm := component.NewComponentManagerBuilder().
    79  		AddWorker(pool.WorkerLogic()).
    80  		Build()
    81  	cm.Start(ctx)
    82  
    83  	unittest.RequireCloseBefore(t, cm.Ready(), 100*time.Millisecond, "could not start worker")
    84  
    85  	require.True(t, pool.Submit("first-event-ever"))
    86  
    87  	// wait for the first event to be picked by the single worker
    88  	unittest.RequireCloseBefore(t, firstEventArrived, 100*time.Millisecond, "first event not distributed")
    89  
    90  	// now the worker is blocked, we submit the rest of the events so that the queue is full
    91  	for i := 0; i < size; i++ {
    92  		event := fmt.Sprintf("test-event-%d", i)
    93  		require.True(t, pool.Submit(event))
    94  		// we also check that re-submitting the same event fails as duplicate event already is in the queue.
    95  		require.False(t, pool.Submit(event))
    96  	}
    97  
    98  	// now the queue is full, so the next submission should fail
    99  	require.False(t, pool.Submit("test-event"))
   100  
   101  	close(blockingChannel)
   102  	cancel()
   103  	unittest.RequireCloseBefore(t, cm.Done(), 100*time.Millisecond, "could not stop worker")
   104  }
   105  
   106  // TestWorkerPool_TwoWorkers_ConcurrentEvents tests the WorkerPoolBuilder with multiple events and two workers.
   107  // It submits multiple events to the WorkerPool concurrently and checks if each event is processed exactly once.
   108  func TestWorkerPool_TwoWorkers_ConcurrentEvents(t *testing.T) {
   109  	size := 10
   110  
   111  	tc := make([]string, size)
   112  
   113  	for i := 0; i < size; i++ {
   114  		tc[i] = fmt.Sprintf("test-event-%d", i)
   115  	}
   116  
   117  	q := queue.NewHeroStore(uint32(size), unittest.Logger(), metrics.NewNoopCollector())
   118  	distributedEvents := unittest.NewProtectedMap[string, struct{}]()
   119  	allEventsDistributed := sync.WaitGroup{}
   120  	allEventsDistributed.Add(size)
   121  
   122  	pool := worker.NewWorkerPoolBuilder[string](
   123  		unittest.Logger(),
   124  		q,
   125  		func(event string) error {
   126  			// check if the event is in the test case
   127  			require.Contains(t, tc, event)
   128  
   129  			// check if the event is distributed only once
   130  			require.False(t, distributedEvents.Has(event))
   131  			distributedEvents.Add(event, struct{}{})
   132  
   133  			allEventsDistributed.Done()
   134  
   135  			return nil
   136  		}).Build()
   137  
   138  	cancelCtx, cancel := context.WithCancel(context.Background())
   139  	defer cancel()
   140  	ctx, _ := irrecoverable.WithSignaler(cancelCtx)
   141  	cm := component.NewComponentManagerBuilder().
   142  		AddWorker(pool.WorkerLogic()).
   143  		AddWorker(pool.WorkerLogic()).
   144  		Build()
   145  	cm.Start(ctx)
   146  
   147  	unittest.RequireCloseBefore(t, cm.Ready(), 100*time.Millisecond, "could not start worker")
   148  
   149  	for i := 0; i < size; i++ {
   150  		go func(i int) {
   151  			require.True(t, pool.Submit(tc[i]))
   152  		}(i)
   153  	}
   154  
   155  	unittest.RequireReturnsBefore(t, allEventsDistributed.Wait, 100*time.Millisecond, "events not processed")
   156  	cancel()
   157  	unittest.RequireCloseBefore(t, cm.Done(), 100*time.Millisecond, "could not stop worker")
   158  }