github.com/koko1123/flow-go-1@v0.29.6/module/jobqueue/consumer_test.go (about) 1 package jobqueue 2 3 import ( 4 "fmt" 5 "strconv" 6 "sync" 7 "testing" 8 "time" 9 10 badgerdb "github.com/dgraph-io/badger/v3" 11 "github.com/rs/zerolog" 12 "github.com/stretchr/testify/require" 13 14 "github.com/koko1123/flow-go-1/module" 15 "github.com/koko1123/flow-go-1/storage" 16 "github.com/koko1123/flow-go-1/storage/badger" 17 "github.com/koko1123/flow-go-1/utils/unittest" 18 ) 19 20 func TestProcessableJobs(t *testing.T) { 21 t.Parallel() 22 23 processedIndex := uint64(2) 24 maxProcessing := uint64(3) 25 maxSearchAhead := uint64(5) 26 27 populate := func(start, end uint64, incomplete []uint64) map[uint64]*jobStatus { 28 processings := map[uint64]*jobStatus{} 29 for i := start; i <= end; i++ { 30 processings[i] = &jobStatus{jobID: JobIDAtIndex(i), done: true} 31 } 32 for _, i := range incomplete { 33 processings[i].done = false 34 } 35 36 return processings 37 } 38 39 t.Run("no job, nothing to process", func(t *testing.T) { 40 jobs := NewMockJobs() // no job in the queue 41 processings := map[uint64]*jobStatus{} 42 processedIndex := uint64(0) 43 44 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, 0, processedIndex) 45 46 require.NoError(t, err) 47 require.Equal(t, uint64(0), processedTo) 48 assertJobs(t, []uint64{}, jobsToRun) 49 }) 50 51 t.Run("max processing was not reached", func(t *testing.T) { 52 jobs := NewMockJobs() 53 require.NoError(t, jobs.PushN(20)) // enough jobs in the queue 54 55 // job 3 are 5 are not done, 2 processing in total 56 // 4, 6, 7, 8, 9, 10, 11 are finished, 7 finished in total 57 processings := populate(3, 11, []uint64{3, 5}) 58 59 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, 0, processedIndex) 60 61 require.NoError(t, err) 62 require.Equal(t, uint64(2), processedTo) 63 // it will process on more job, and reach the max processing. 64 assertJobs(t, []uint64{ 65 12, 66 }, jobsToRun) 67 }) 68 69 t.Run("reached max processing", func(t *testing.T) { 70 jobs := NewMockJobs() 71 require.NoError(t, jobs.PushN(20)) // enough jobs in the queue 72 73 // job 3, 5, 6 are not done, which have reached max processing(3) 74 // 4, 7, 8, 9, 10, 11, 12 are finished, 7 finished in total 75 processings := populate(3, 12, []uint64{3, 5, 6}) 76 77 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, 0, processedIndex) 78 79 require.NoError(t, err) 80 require.Equal(t, uint64(2), processedTo) 81 // it will not process any job, because the max processing is reached. 82 assertJobs(t, []uint64{}, jobsToRun) 83 }) 84 85 t.Run("processing pauses and resumes", func(t *testing.T) { 86 jobs := NewMockJobs() 87 require.NoError(t, jobs.PushN(20)) // enough jobs in the queue 88 89 maxProcessing := uint64(4) 90 91 // job 3, 5 are not done 92 // 4, 6, 7 are finished, 3 finished in total 93 processings := populate(3, processedIndex+maxSearchAhead, []uint64{3, 5}) 94 95 // it will not process any job, because the consumer is paused 96 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, maxSearchAhead, processedIndex) 97 98 require.NoError(t, err) 99 require.Equal(t, processedIndex, processedTo) 100 assertJobs(t, []uint64{}, jobsToRun) 101 102 // lowest job is processed, which should cause consumer to resume 103 processings[uint64(3)].done = true 104 105 // Job 3 is done, so it should return 2 more jobs 8-9 and pause again with one available worker 106 jobsToRun, processedTo, err = processableJobs(jobs, processings, maxProcessing, maxSearchAhead, processedIndex) 107 108 require.NoError(t, err) 109 require.Equal(t, uint64(4), processedTo) 110 assertJobs(t, []uint64{8, 9}, jobsToRun) 111 112 // lowest job is processed, which should cause consumer to resume 113 processings[uint64(5)].done = true 114 115 // job 5 is processed, it should return jobs 8-11 (one job for each worker) 116 jobsToRun, processedTo, err = processableJobs(jobs, processings, maxProcessing, maxSearchAhead, processedIndex) 117 118 require.NoError(t, err) 119 require.Equal(t, uint64(7), processedTo) 120 assertJobs(t, []uint64{8, 9, 10, 11}, jobsToRun) 121 }) 122 123 t.Run("no more job", func(t *testing.T) { 124 jobs := NewMockJobs() 125 require.NoError(t, jobs.PushN(11)) // 11 jobs, no more job to process 126 127 // job 3, 11 are not done, which have not reached max processing (3) 128 // 4, 5, 6, 7, 8, 9, 10 are finished, 7 finished in total 129 processings := populate(3, 11, []uint64{3, 11}) 130 131 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, 0, processedIndex) 132 133 require.NoError(t, err) 134 require.Equal(t, uint64(2), processedTo) 135 assertJobs(t, []uint64{}, jobsToRun) 136 }) 137 138 t.Run("next jobs were done", func(t *testing.T) { 139 jobs := NewMockJobs() 140 require.NoError(t, jobs.PushN(20)) // enough jobs in the queue 141 142 // job 3, 5 are done 143 // job 4, 6 are not done, which have not reached max processing 144 processings := populate(3, 6, []uint64{4, 6}) 145 146 jobsToRun, processedTo, err := processableJobs(jobs, processings, maxProcessing, 0, processedIndex) 147 148 require.NoError(t, err) 149 require.Equal(t, uint64(3), processedTo) 150 assertJobs(t, []uint64{ 151 7, 152 }, jobsToRun) 153 }) 154 155 } 156 157 // Test after jobs have been processed, the job status are removed to prevent from memory-leak 158 func TestProcessedIndexDeletion(t *testing.T) { 159 setup := func(t *testing.T, f func(c *Consumer, jobs *MockJobs)) { 160 unittest.RunWithBadgerDB(t, func(db *badgerdb.DB) { 161 log := unittest.Logger().With().Str("module", "consumer").Logger() 162 jobs := NewMockJobs() 163 progress := badger.NewConsumerProgress(db, "consumer") 164 worker := newMockWorker() 165 maxProcessing := uint64(3) 166 c := NewConsumer(log, jobs, progress, worker, maxProcessing, 0) 167 worker.WithConsumer(c) 168 169 f(c, jobs) 170 }) 171 } 172 173 setup(t, func(c *Consumer, jobs *MockJobs) { 174 require.NoError(t, jobs.PushN(10)) 175 require.NoError(t, c.Start(0)) 176 177 require.Eventually(t, func() bool { 178 c.mu.Lock() 179 defer c.mu.Unlock() 180 return c.processedIndex == uint64(10) 181 }, 2*time.Second, 10*time.Millisecond) 182 183 // should have no processing after all jobs are processed 184 c.mu.Lock() 185 defer c.mu.Unlock() 186 require.Len(t, c.processings, 0) 187 require.Len(t, c.processingsIndex, 0) 188 }) 189 } 190 191 func assertJobs(t *testing.T, expectedIndex []uint64, jobsToRun []*jobAtIndex) { 192 actualIndex := make([]uint64, 0, len(jobsToRun)) 193 for _, jobAtIndex := range jobsToRun { 194 require.NotNil(t, jobAtIndex.job) 195 actualIndex = append(actualIndex, jobAtIndex.index) 196 } 197 require.Equal(t, expectedIndex, actualIndex) 198 } 199 200 // MockJobs implements the Jobs int64erface, and is used as the dependency for 201 // the Consumer for testing purpose 202 type MockJobs struct { 203 sync.Mutex 204 log zerolog.Logger 205 last int 206 jobs map[int]module.Job 207 index map[module.JobID]int 208 JobMaker *JobMaker 209 } 210 211 func NewMockJobs() *MockJobs { 212 return &MockJobs{ 213 log: unittest.Logger().With().Str("module", "jobs").Logger(), 214 last: 0, // must be from 1 215 jobs: make(map[int]module.Job), 216 index: make(map[module.JobID]int), 217 JobMaker: NewJobMaker(), 218 } 219 } 220 221 func (j *MockJobs) AtIndex(index uint64) (module.Job, error) { 222 j.Lock() 223 defer j.Unlock() 224 225 job, ok := j.jobs[int(index)] 226 227 j.log.Debug().Int("index", int(index)).Bool("exists", ok).Msg("reading job at index") 228 229 if !ok { 230 return nil, storage.ErrNotFound 231 } 232 233 return job, nil 234 } 235 236 func (j *MockJobs) Head() (uint64, error) { 237 return uint64(j.last), nil 238 } 239 240 func (j *MockJobs) Add(job module.Job) error { 241 j.Lock() 242 defer j.Unlock() 243 244 j.log.Debug().Str("job_id", string(job.ID())).Msg("adding job") 245 246 id := job.ID() 247 _, ok := j.index[id] 248 if ok { 249 return storage.ErrAlreadyExists 250 } 251 252 index := j.last + 1 253 j.index[id] = int(index) 254 j.jobs[index] = job 255 j.last++ 256 257 j.log. 258 Debug().Str("job_id", string(job.ID())). 259 Int("index", index). 260 Msg("job added at index") 261 262 return nil 263 } 264 265 func (j *MockJobs) PushOne() error { 266 job := j.JobMaker.Next() 267 return j.Add(job) 268 } 269 270 func (j *MockJobs) PushN(n int64) error { 271 for i := 0; i < int(n); i++ { 272 err := j.PushOne() 273 if err != nil { 274 return err 275 } 276 } 277 return nil 278 } 279 280 // deterministically compute the JobID from index 281 func JobIDAtIndex(index uint64) module.JobID { 282 return module.JobID(fmt.Sprintf("%v", index)) 283 } 284 285 func JobIDToIndex(id module.JobID) (uint64, error) { 286 return strconv.ParseUint(string(id), 10, 64) 287 } 288 289 // JobMaker is a test helper. 290 // it creates new job with unique job id 291 type JobMaker struct { 292 sync.Mutex 293 index uint64 294 } 295 296 func NewJobMaker() *JobMaker { 297 return &JobMaker{ 298 index: 1, 299 } 300 } 301 302 type TestJob struct { 303 index uint64 304 } 305 306 func (tj TestJob) ID() module.JobID { 307 return JobIDAtIndex(tj.index) 308 } 309 310 // return next unique job 311 func (j *JobMaker) Next() module.Job { 312 j.Lock() 313 defer j.Unlock() 314 315 job := &TestJob{ 316 index: j.index, 317 } 318 j.index++ 319 return job 320 } 321 322 type mockWorker struct { 323 consumer *Consumer 324 } 325 326 func newMockWorker() *mockWorker { 327 return &mockWorker{} 328 } 329 330 func (w *mockWorker) WithConsumer(c *Consumer) { 331 w.consumer = c 332 } 333 334 func (w *mockWorker) Run(job module.Job) error { 335 w.consumer.NotifyJobIsDone(job.ID()) 336 return nil 337 }