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  }