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  }