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

     1  package blockconsumer_test
     2  
     3  import (
     4  	"sync"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/dgraph-io/badger/v2"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    12  	"github.com/onflow/flow-go/engine/testutil"
    13  	"github.com/onflow/flow-go/engine/verification/assigner/blockconsumer"
    14  	vertestutils "github.com/onflow/flow-go/engine/verification/utils/unittest"
    15  	"github.com/onflow/flow-go/model/flow"
    16  	"github.com/onflow/flow-go/model/flow/filter"
    17  	"github.com/onflow/flow-go/module"
    18  	"github.com/onflow/flow-go/module/jobqueue"
    19  	"github.com/onflow/flow-go/module/metrics"
    20  	"github.com/onflow/flow-go/module/trace"
    21  	bstorage "github.com/onflow/flow-go/storage/badger"
    22  	"github.com/onflow/flow-go/utils/unittest"
    23  )
    24  
    25  // TestBlockToJob evaluates that a block can be converted to a job,
    26  // and its corresponding job can be converted back to the same block.
    27  func TestBlockToJob(t *testing.T) {
    28  	block := unittest.BlockFixture()
    29  	actual, err := jobqueue.JobToBlock(jobqueue.BlockToJob(&block))
    30  	require.NoError(t, err)
    31  	require.Equal(t, &block, actual)
    32  }
    33  
    34  func TestProduceConsume(t *testing.T) {
    35  	// pushing 10 finalized blocks sequentially to block reader, with 3 workers on consumer and the block processor
    36  	// blocking on the blocks, results in processor only receiving the first three finalized blocks:
    37  	// 10 blocks sequentially --> block reader --> consumer can read and push 3 blocks at a time to processor --> blocking processor.
    38  	t.Run("pushing 10 blocks, blocking, receives 3", func(t *testing.T) {
    39  		received := make([]*flow.Block, 0)
    40  		lock := &sync.Mutex{}
    41  		neverFinish := func(notifier module.ProcessingNotifier, block *flow.Block) {
    42  			lock.Lock()
    43  			defer lock.Unlock()
    44  			received = append(received, block)
    45  
    46  			// this processor never notifies consumer that it is done with the block.
    47  			// hence from consumer perspective, it is blocking on each received block.
    48  		}
    49  
    50  		withConsumer(t, 10, 3, neverFinish, func(consumer *blockconsumer.BlockConsumer, blocks []*flow.Block) {
    51  			unittest.RequireCloseBefore(t, consumer.Ready(), time.Second, "could not start consumer")
    52  
    53  			for i := 0; i < len(blocks); i++ {
    54  				// consumer is only required to be "notified" that a new finalized block available.
    55  				// It keeps track of the last finalized block it has read, and read the next height upon
    56  				// getting notified as follows:
    57  				consumer.OnFinalizedBlock(&model.Block{})
    58  			}
    59  
    60  			unittest.RequireCloseBefore(t, consumer.Done(), time.Second, "could not terminate consumer")
    61  
    62  			// expects the processor receive only the first 3 blocks (since it is blocked on those, hence no
    63  			// new block is fetched to process).
    64  			require.ElementsMatch(t, flow.GetIDs(blocks[:3]), flow.GetIDs(received))
    65  		})
    66  	})
    67  
    68  	// pushing 10 finalized blocks sequentially to block reader, with 3 workers on consumer and the processor finishes processing
    69  	// each received block immediately, results in processor receiving all 10 blocks:
    70  	// 10 blocks sequentially --> block reader --> consumer can read and push 3 blocks at a time to processor --> processor finishes blocks
    71  	// immediately.
    72  	t.Run("pushing 100 blocks, non-blocking, receives 100", func(t *testing.T) {
    73  		received := make([]*flow.Block, 0)
    74  		lock := &sync.Mutex{}
    75  		var processAll sync.WaitGroup
    76  		alwaysFinish := func(notifier module.ProcessingNotifier, block *flow.Block) {
    77  			lock.Lock()
    78  			defer lock.Unlock()
    79  
    80  			received = append(received, block)
    81  
    82  			go func() {
    83  				notifier.Notify(block.ID())
    84  				processAll.Done()
    85  			}()
    86  		}
    87  
    88  		withConsumer(t, 100, 3, alwaysFinish, func(consumer *blockconsumer.BlockConsumer, blocks []*flow.Block) {
    89  			unittest.RequireCloseBefore(t, consumer.Ready(), time.Second, "could not start consumer")
    90  			processAll.Add(len(blocks))
    91  
    92  			for i := 0; i < len(blocks); i++ {
    93  				// consumer is only required to be "notified" that a new finalized block available.
    94  				// It keeps track of the last finalized block it has read, and read the next height upon
    95  				// getting notified as follows:
    96  				consumer.OnFinalizedBlock(&model.Block{})
    97  			}
    98  
    99  			// waits until all blocks finish processing
   100  			unittest.RequireReturnsBefore(t, processAll.Wait, time.Second, "could not process all blocks on time")
   101  			unittest.RequireCloseBefore(t, consumer.Done(), time.Second, "could not terminate consumer")
   102  
   103  			// expects the mock engine receive all 100 blocks.
   104  			require.ElementsMatch(t, flow.GetIDs(blocks), flow.GetIDs(received))
   105  		})
   106  	})
   107  
   108  }
   109  
   110  // withConsumer is a test helper that sets up a block consumer with specified number of workers.
   111  // The block consumer operates on a block reader with a chain of specified number of finalized blocks
   112  // ready to read and consumer.
   113  func withConsumer(
   114  	t *testing.T,
   115  	blockCount int,
   116  	workerCount int,
   117  	process func(notifier module.ProcessingNotifier, block *flow.Block),
   118  	withBlockConsumer func(*blockconsumer.BlockConsumer, []*flow.Block),
   119  ) {
   120  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
   121  		maxProcessing := uint64(workerCount)
   122  
   123  		processedHeight := bstorage.NewConsumerProgress(db, module.ConsumeProgressVerificationBlockHeight)
   124  		collector := &metrics.NoopCollector{}
   125  		tracer := trace.NewNoopTracer()
   126  		log := unittest.Logger()
   127  		participants := unittest.IdentityListFixture(5, unittest.WithAllRoles())
   128  		rootSnapshot := unittest.RootSnapshotFixture(participants)
   129  		s := testutil.CompleteStateFixture(t, log, collector, tracer, rootSnapshot)
   130  
   131  		engine := &mockBlockProcessor{
   132  			process: process,
   133  		}
   134  
   135  		consumer, _, err := blockconsumer.NewBlockConsumer(
   136  			unittest.Logger(),
   137  			collector,
   138  			processedHeight,
   139  			s.Storage.Blocks,
   140  			s.State,
   141  			engine,
   142  			maxProcessing)
   143  		require.NoError(t, err)
   144  
   145  		// generates a chain of blocks in the form of root <- R1 <- C1 <- R2 <- C2 <- ... where Rs are distinct reference
   146  		// blocks (i.e., containing guarantees), and Cs are container blocks for their preceding reference block,
   147  		// Container blocks only contain receipts of their preceding reference blocks. But they do not
   148  		// hold any guarantees.
   149  		root, err := s.State.Final().Head()
   150  		require.NoError(t, err)
   151  		rootProtocolState, err := s.State.Final().ProtocolState()
   152  		require.NoError(t, err)
   153  		rootProtocolStateID := rootProtocolState.ID()
   154  		clusterCommittee := participants.Filter(filter.HasRole[flow.Identity](flow.RoleCollection))
   155  		sources := unittest.RandomSourcesFixture(110)
   156  		results := vertestutils.CompleteExecutionReceiptChainFixture(
   157  			t,
   158  			root,
   159  			rootProtocolStateID,
   160  			blockCount/2,
   161  			sources,
   162  			vertestutils.WithClusterCommittee(clusterCommittee),
   163  		)
   164  		blocks := vertestutils.ExtendStateWithFinalizedBlocks(t, results, s.State)
   165  		// makes sure that we generated a block chain of requested length.
   166  		require.Len(t, blocks, blockCount)
   167  
   168  		withBlockConsumer(consumer, blocks)
   169  	})
   170  }
   171  
   172  // mockBlockProcessor provides a FinalizedBlockProcessor with a plug-and-play process method.
   173  type mockBlockProcessor struct {
   174  	notifier module.ProcessingNotifier
   175  	process  func(module.ProcessingNotifier, *flow.Block)
   176  }
   177  
   178  func (e *mockBlockProcessor) ProcessFinalizedBlock(block *flow.Block) {
   179  	e.process(e.notifier, block)
   180  }
   181  
   182  func (e *mockBlockProcessor) WithBlockConsumerNotifier(notifier module.ProcessingNotifier) {
   183  	e.notifier = notifier
   184  }