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