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 }