github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/verification/fetcher/chunkconsumer/consumer_test.go (about)

     1  package chunkconsumer_test
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"sync"
     7  	"testing"
     8  
     9  	"github.com/dgraph-io/badger/v2"
    10  	"github.com/stretchr/testify/require"
    11  	"go.uber.org/atomic"
    12  
    13  	"github.com/onflow/flow-go/engine/verification/fetcher/chunkconsumer"
    14  	"github.com/onflow/flow-go/model/chunks"
    15  	"github.com/onflow/flow-go/module"
    16  	"github.com/onflow/flow-go/module/metrics"
    17  	storage "github.com/onflow/flow-go/storage/badger"
    18  	"github.com/onflow/flow-go/utils/unittest"
    19  )
    20  
    21  // TestChunkLocatorToJob evaluates that a chunk locator can be converted to a job,
    22  // and its corresponding job can be converted back to the same locator.
    23  func TestChunkLocatorToJob(t *testing.T) {
    24  	locator := unittest.ChunkLocatorFixture(unittest.IdentifierFixture(), rand.Uint64())
    25  	actual, err := chunkconsumer.JobToChunkLocator(chunkconsumer.ChunkLocatorToJob(locator))
    26  	require.NoError(t, err)
    27  	require.Equal(t, locator, actual)
    28  }
    29  
    30  // TestProduceConsume evaluates different scenarios on passing jobs to chunk queue with 3 workers on the consumer side. It evaluates blocking and
    31  // none-blocking engines attached to the workers in sequential and concurrent scenarios.
    32  func TestProduceConsume(t *testing.T) {
    33  	// pushing 10 jobs sequentially to chunk queue, with 3 workers on consumer and the engine blocking on the jobs,
    34  	// results in engine only receiving 3 jobs.
    35  	t.Run("pushing 10 jobs receive 3", func(t *testing.T) {
    36  		var called chunks.LocatorList
    37  		lock := &sync.Mutex{}
    38  		neverFinish := func(notifier module.ProcessingNotifier, locator *chunks.Locator) {
    39  			lock.Lock()
    40  			defer lock.Unlock()
    41  			called = append(called, locator)
    42  		}
    43  		WithConsumer(t, neverFinish, func(consumer *chunkconsumer.ChunkConsumer, chunksQueue *storage.ChunksQueue) {
    44  			<-consumer.Ready()
    45  
    46  			locators := unittest.ChunkLocatorListFixture(10)
    47  
    48  			for i, locator := range locators {
    49  				ok, err := chunksQueue.StoreChunkLocator(locator)
    50  				require.NoError(t, err, fmt.Sprintf("chunk locator %v can't be stored", i))
    51  				require.True(t, ok)
    52  				consumer.Check() // notify the consumer
    53  			}
    54  
    55  			<-consumer.Done()
    56  
    57  			// expect the mock engine receive only the first 3 calls (since it is blocked on those, hence no
    58  			// new job is fetched to process).
    59  			require.Equal(t, locators[:3], called)
    60  		})
    61  	})
    62  
    63  	// pushing 10 jobs sequentially to chunk queue, with 3 workers on consumer and the engine immediately finishing the job,
    64  	// results in engine eventually receiving all 10 jobs.
    65  	t.Run("pushing 10 receive 10", func(t *testing.T) {
    66  		var called chunks.LocatorList
    67  		lock := &sync.Mutex{}
    68  		var finishAll sync.WaitGroup
    69  		alwaysFinish := func(notifier module.ProcessingNotifier, locator *chunks.Locator) {
    70  			lock.Lock()
    71  			defer lock.Unlock()
    72  			called = append(called, locator)
    73  			finishAll.Add(1)
    74  			go func() {
    75  				notifier.Notify(locator.ID())
    76  				finishAll.Done()
    77  			}()
    78  		}
    79  		WithConsumer(t, alwaysFinish, func(consumer *chunkconsumer.ChunkConsumer, chunksQueue *storage.ChunksQueue) {
    80  			<-consumer.Ready()
    81  
    82  			locators := unittest.ChunkLocatorListFixture(10)
    83  
    84  			for i, locator := range locators {
    85  				ok, err := chunksQueue.StoreChunkLocator(locator)
    86  				require.NoError(t, err, fmt.Sprintf("chunk locator %v can't be stored", i))
    87  				require.True(t, ok)
    88  				consumer.Check() // notify the consumer
    89  			}
    90  
    91  			<-consumer.Done()
    92  			finishAll.Wait() // wait until all finished
    93  			// expect the mock engine receives all 10 calls
    94  			require.Equal(t, locators, called)
    95  		})
    96  	})
    97  
    98  	// pushing 100 jobs concurrently to chunk queue, with 3 workers on consumer and the engine immediately finishing the job,
    99  	// results in engine eventually receiving all 100 jobs.
   100  	t.Run("pushing 100 concurrently receive 100", func(t *testing.T) {
   101  		var called chunks.LocatorList
   102  		lock := &sync.Mutex{}
   103  		var finishAll sync.WaitGroup
   104  		finishAll.Add(100)
   105  		alwaysFinish := func(notifier module.ProcessingNotifier, locator *chunks.Locator) {
   106  			lock.Lock()
   107  			defer lock.Unlock()
   108  			called = append(called, locator)
   109  			go func() {
   110  				notifier.Notify(locator.ID())
   111  				finishAll.Done()
   112  			}()
   113  		}
   114  		WithConsumer(t, alwaysFinish, func(consumer *chunkconsumer.ChunkConsumer, chunksQueue *storage.ChunksQueue) {
   115  			<-consumer.Ready()
   116  			total := atomic.NewUint32(0)
   117  
   118  			locators := unittest.ChunkLocatorListFixture(100)
   119  
   120  			for i := 0; i < len(locators); i++ {
   121  				go func(i int) {
   122  					ok, err := chunksQueue.StoreChunkLocator(locators[i])
   123  					require.NoError(t, err, fmt.Sprintf("chunk locator %v can't be stored", i))
   124  					require.True(t, ok)
   125  					total.Inc()
   126  					consumer.Check() // notify the consumer
   127  				}(i)
   128  			}
   129  
   130  			finishAll.Wait()
   131  			<-consumer.Done()
   132  
   133  			// expect the mock engine receives all 100 calls
   134  			require.Equal(t, uint32(100), total.Load())
   135  		})
   136  	})
   137  }
   138  
   139  func WithConsumer(
   140  	t *testing.T,
   141  	process func(module.ProcessingNotifier, *chunks.Locator),
   142  	withConsumer func(*chunkconsumer.ChunkConsumer, *storage.ChunksQueue),
   143  ) {
   144  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
   145  		maxProcessing := uint64(3)
   146  
   147  		processedIndex := storage.NewConsumerProgress(db, module.ConsumeProgressVerificationChunkIndex)
   148  		chunksQueue := storage.NewChunkQueue(db)
   149  		ok, err := chunksQueue.Init(chunkconsumer.DefaultJobIndex)
   150  		require.NoError(t, err)
   151  		require.True(t, ok)
   152  
   153  		engine := &mockChunkProcessor{
   154  			process: process,
   155  		}
   156  
   157  		collector := &metrics.NoopCollector{}
   158  		consumer, err := chunkconsumer.NewChunkConsumer(
   159  			unittest.Logger(),
   160  			collector,
   161  			processedIndex,
   162  			chunksQueue,
   163  			engine,
   164  			maxProcessing,
   165  		)
   166  		require.NoError(t, err)
   167  
   168  		withConsumer(consumer, chunksQueue)
   169  	})
   170  }
   171  
   172  // mockChunkProcessor provides an AssignedChunkProcessor with a plug-and-play process method.
   173  type mockChunkProcessor struct {
   174  	notifier module.ProcessingNotifier
   175  	process  func(notifier module.ProcessingNotifier, locator *chunks.Locator)
   176  }
   177  
   178  func (e *mockChunkProcessor) Ready() <-chan struct{} {
   179  	ready := make(chan struct{})
   180  	close(ready)
   181  	return ready
   182  }
   183  
   184  func (e *mockChunkProcessor) Done() <-chan struct{} {
   185  	done := make(chan struct{})
   186  	close(done)
   187  	return done
   188  }
   189  
   190  func (e *mockChunkProcessor) ProcessAssignedChunk(locator *chunks.Locator) {
   191  	e.process(e.notifier, locator)
   192  }
   193  
   194  func (e *mockChunkProcessor) WithChunkConsumerNotifier(notifier module.ProcessingNotifier) {
   195  	e.notifier = notifier
   196  }