github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/jobqueue/component_consumer_test.go (about) 1 package jobqueue 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/rs/zerolog" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 "github.com/stretchr/testify/suite" 16 "go.uber.org/atomic" 17 18 "github.com/onflow/flow-go/module" 19 "github.com/onflow/flow-go/module/irrecoverable" 20 modulemock "github.com/onflow/flow-go/module/mock" 21 "github.com/onflow/flow-go/storage" 22 storagemock "github.com/onflow/flow-go/storage/mock" 23 "github.com/onflow/flow-go/utils/unittest" 24 ) 25 26 type ComponentConsumerSuite struct { 27 suite.Suite 28 29 defaultIndex uint64 30 maxProcessing uint64 31 maxSearchAhead uint64 32 } 33 34 func TestComponentConsumerSuite(t *testing.T) { 35 t.Parallel() 36 suite.Run(t, new(ComponentConsumerSuite)) 37 } 38 39 func (suite *ComponentConsumerSuite) SetupTest() { 40 suite.defaultIndex = uint64(0) 41 suite.maxProcessing = uint64(2) 42 suite.maxSearchAhead = uint64(5) 43 } 44 45 func mockJobs(data map[uint64]TestJob) *modulemock.Jobs { 46 jobs := new(modulemock.Jobs) 47 48 jobs.On("AtIndex", mock.AnythingOfType("uint64")).Return( 49 func(index uint64) module.Job { 50 job, ok := data[index] 51 if !ok { 52 return nil 53 } 54 return job 55 }, 56 func(index uint64) error { 57 _, ok := data[index] 58 if !ok { 59 return storage.ErrNotFound 60 } 61 return nil 62 }, 63 ) 64 65 return jobs 66 } 67 68 func generateTestData(jobCount uint64) map[uint64]TestJob { 69 jobData := make(map[uint64]TestJob, jobCount) 70 71 for i := uint64(1); i <= jobCount; i++ { 72 jobData[i] = TestJob{i} 73 } 74 75 return jobData 76 } 77 78 func (suite *ComponentConsumerSuite) prepareTest( 79 processor JobProcessor, 80 preNotifier NotifyDone, 81 postNotifier NotifyDone, 82 jobData map[uint64]TestJob, 83 ) (*ComponentConsumer, chan struct{}) { 84 85 jobs := mockJobs(jobData) 86 workSignal := make(chan struct{}, 1) 87 88 progress := new(storagemock.ConsumerProgress) 89 progress.On("ProcessedIndex").Return(suite.defaultIndex, nil) 90 progress.On("SetProcessedIndex", mock.AnythingOfType("uint64")).Return(nil) 91 92 consumer, err := NewComponentConsumer( 93 zerolog.New(os.Stdout).With().Timestamp().Logger(), 94 workSignal, 95 progress, 96 jobs, 97 suite.defaultIndex, 98 processor, 99 suite.maxProcessing, 100 suite.maxSearchAhead, 101 ) 102 require.NoError(suite.T(), err) 103 consumer.SetPreNotifier(preNotifier) 104 consumer.SetPostNotifier(postNotifier) 105 106 return consumer, workSignal 107 } 108 109 // TestHappyPath: 110 // - processes jobs until cancelled 111 // - notify called for all jobs 112 func (suite *ComponentConsumerSuite) TestHappyPath() { 113 testCtx, testCancel := context.WithCancel(context.Background()) 114 defer testCancel() 115 116 testJobsCount := uint64(20) 117 jobData := generateTestData(testJobsCount) 118 finishedJobs := make(map[uint64]bool, testJobsCount) 119 120 wg := sync.WaitGroup{} 121 mu := sync.Mutex{} 122 123 processor := func(_ irrecoverable.SignalerContext, _ module.Job, complete func()) { complete() } 124 notifier := func(jobID module.JobID) { 125 defer wg.Done() 126 127 index, err := JobIDToIndex(jobID) 128 assert.NoError(suite.T(), err) 129 130 mu.Lock() 131 defer mu.Unlock() 132 finishedJobs[index] = true 133 134 suite.T().Logf("job %d finished", index) 135 } 136 137 suite.Run("runs and notifies using pre-notifier", func() { 138 wg.Add(int(testJobsCount)) 139 consumer, workSignal := suite.prepareTest(processor, nil, notifier, jobData) 140 141 suite.runTest(testCtx, consumer, func() { 142 workSignal <- struct{}{} 143 wg.Wait() 144 }) 145 146 // verify all jobs were run 147 mu.Lock() 148 defer mu.Unlock() 149 assert.Len(suite.T(), finishedJobs, len(jobData)) 150 for index := range jobData { 151 assert.True(suite.T(), finishedJobs[index], "job %d did not finished", index) 152 } 153 }) 154 155 suite.Run("runs and notifies using post-notifier", func() { 156 wg.Add(int(testJobsCount)) 157 consumer, workSignal := suite.prepareTest(processor, notifier, nil, jobData) 158 159 suite.runTest(testCtx, consumer, func() { 160 workSignal <- struct{}{} 161 wg.Wait() 162 }) 163 164 // verify all jobs were run 165 mu.Lock() 166 defer mu.Unlock() 167 assert.Len(suite.T(), finishedJobs, len(jobData)) 168 for index := range jobData { 169 assert.True(suite.T(), finishedJobs[index], "job %d did not finished", index) 170 } 171 }) 172 } 173 174 // TestProgressesOnComplete: 175 // - only processes next job after complete is called 176 func (suite *ComponentConsumerSuite) TestProgressesOnComplete() { 177 testCtx, testCancel := context.WithCancel(context.Background()) 178 defer testCancel() 179 180 stopIndex := uint64(10) 181 testJobsCount := uint64(11) 182 jobData := generateTestData(testJobsCount) 183 finishedJobs := make(map[uint64]bool, testJobsCount) 184 185 mu := sync.Mutex{} 186 done := make(chan struct{}) 187 188 processor := func(_ irrecoverable.SignalerContext, job module.Job, complete func()) { 189 index, err := JobIDToIndex(job.ID()) 190 assert.NoError(suite.T(), err) 191 192 if index <= stopIndex { 193 complete() 194 } 195 } 196 notifier := func(jobID module.JobID) { 197 index, err := JobIDToIndex(jobID) 198 assert.NoError(suite.T(), err) 199 200 mu.Lock() 201 defer mu.Unlock() 202 finishedJobs[index] = true 203 204 suite.T().Logf("job %d finished", index) 205 if index == stopIndex+1 { 206 close(done) 207 } 208 } 209 210 suite.maxProcessing = 1 211 consumer, workSignal := suite.prepareTest(processor, nil, notifier, jobData) 212 213 suite.runTest(testCtx, consumer, func() { 214 workSignal <- struct{}{} 215 unittest.RequireNeverClosedWithin(suite.T(), done, 100*time.Millisecond, fmt.Sprintf("job %d wasn't supposed to finish", stopIndex+1)) 216 }) 217 218 // verify all jobs were run 219 mu.Lock() 220 defer mu.Unlock() 221 assert.Len(suite.T(), finishedJobs, int(stopIndex)) 222 for index := range finishedJobs { 223 assert.LessOrEqual(suite.T(), index, stopIndex) 224 } 225 } 226 227 func (suite *ComponentConsumerSuite) TestSignalsBeforeReadyDoNotCheck() { 228 testCtx, testCancel := context.WithCancel(context.Background()) 229 defer testCancel() 230 231 suite.defaultIndex = uint64(100) 232 started := atomic.NewBool(false) 233 234 jobConsumer := modulemock.NewJobConsumer(suite.T()) 235 jobConsumer.On("Start").Return(func() error { 236 // force Start to take a while so the processingLoop is ready first 237 // the processingLoop should wait to start, otherwise Check would be called 238 time.Sleep(500 * time.Millisecond) 239 started.Store(true) 240 return nil 241 }) 242 jobConsumer.On("Stop") 243 244 wg := sync.WaitGroup{} 245 wg.Add(1) 246 247 jobConsumer.On("Check").Run(func(_ mock.Arguments) { 248 assert.True(suite.T(), started.Load(), "check was called before started") 249 wg.Done() 250 }) 251 252 consumer, workSignal := suite.prepareTest(nil, nil, nil, nil) 253 consumer.consumer = jobConsumer 254 255 // send a signal before the component starts to ensure Check would be called if the 256 // processingLoop was started 257 workSignal <- struct{}{} 258 259 ctx, cancel := context.WithCancel(testCtx) 260 signalCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx) 261 consumer.Start(signalCtx) 262 263 unittest.RequireCloseBefore(suite.T(), consumer.Ready(), 1*time.Second, "timeout waiting for consumer to be ready") 264 unittest.RequireReturnsBefore(suite.T(), wg.Wait, 100*time.Millisecond, "check was not called") 265 cancel() 266 unittest.RequireCloseBefore(suite.T(), consumer.Done(), 1*time.Second, "timeout waiting for consumer to be done") 267 } 268 269 // TestPassesIrrecoverableErrors: 270 // - throws an irrecoverable error 271 // - verifies no jobs were processed 272 func (suite *ComponentConsumerSuite) TestPassesIrrecoverableErrors() { 273 testCtx, testCancel := context.WithCancel(context.Background()) 274 defer testCancel() 275 276 testJobsCount := uint64(10) 277 jobData := generateTestData(testJobsCount) 278 done := make(chan struct{}) 279 280 expectedErr := fmt.Errorf("test failure") 281 282 // always throws an error 283 processor := func(ctx irrecoverable.SignalerContext, job module.Job, _ func()) { 284 ctx.Throw(expectedErr) 285 } 286 287 // never expects a job 288 notifier := func(jobID module.JobID) { 289 suite.T().Logf("job %s finished unexpectedly", jobID) 290 close(done) 291 } 292 293 consumer, _ := suite.prepareTest(processor, nil, notifier, jobData) 294 295 ctx, cancel := context.WithCancel(testCtx) 296 signalCtx, errChan := irrecoverable.WithSignaler(ctx) 297 298 consumer.Start(signalCtx) 299 unittest.RequireCloseBefore(suite.T(), consumer.Ready(), 100*time.Millisecond, "timeout waiting for consumer to be ready") 300 301 // send job signal, then wait for the irrecoverable error 302 // don't need to sent signal since the worker is kicked off by Start() 303 select { 304 case <-ctx.Done(): 305 suite.T().Errorf("expected irrecoverable error, but got none") 306 case err := <-errChan: 307 assert.ErrorIs(suite.T(), err, expectedErr) 308 } 309 310 // shutdown 311 cancel() 312 unittest.RequireCloseBefore(suite.T(), consumer.Done(), 100*time.Millisecond, "timeout waiting for consumer to be done") 313 314 // no notification should have been sent 315 unittest.RequireNotClosed(suite.T(), done, "job wasn't supposed to finish") 316 } 317 318 func (suite *ComponentConsumerSuite) runTest( 319 testCtx context.Context, 320 consumer *ComponentConsumer, 321 sendJobs func(), 322 ) { 323 ctx, cancel := context.WithCancel(testCtx) 324 signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx) 325 326 consumer.Start(signalerCtx) 327 unittest.RequireCloseBefore(suite.T(), consumer.Ready(), 100*time.Millisecond, "timeout waiting for the consumer to be ready") 328 329 sendJobs() 330 331 // shutdown 332 cancel() 333 unittest.RequireCloseBefore(suite.T(), consumer.Done(), 100*time.Millisecond, "timeout waiting for the consumer to be done") 334 }