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  }