github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/votecollector/vote_cache_test.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    13  	"github.com/onflow/flow-go/utils/unittest"
    14  )
    15  
    16  // TestVotesCache_View tests that View returns same value that was set by constructor
    17  func TestVotesCache_View(t *testing.T) {
    18  	view := uint64(100)
    19  	cache := NewVotesCache(view)
    20  	require.Equal(t, view, cache.View())
    21  }
    22  
    23  // TestVotesCache_AddVoteRepeatedVote tests that AddVote skips duplicated votes
    24  func TestVotesCache_AddVoteRepeatedVote(t *testing.T) {
    25  	t.Parallel()
    26  
    27  	view := uint64(100)
    28  	cache := NewVotesCache(view)
    29  	vote := unittest.VoteFixture(unittest.WithVoteView(view))
    30  
    31  	require.NoError(t, cache.AddVote(vote))
    32  	err := cache.AddVote(vote)
    33  	require.ErrorIs(t, err, RepeatedVoteErr)
    34  }
    35  
    36  // TestVotesCache_AddVoteIncompatibleView tests that adding vote with incompatible view results in error
    37  func TestVotesCache_AddVoteIncompatibleView(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	view := uint64(100)
    41  	cache := NewVotesCache(view)
    42  	vote := unittest.VoteFixture(unittest.WithVoteView(view + 1))
    43  	err := cache.AddVote(vote)
    44  	require.ErrorIs(t, err, VoteForIncompatibleViewError)
    45  }
    46  
    47  // TestVotesCache_GetVote tests that GetVote method
    48  func TestVotesCache_GetVote(t *testing.T) {
    49  	view := uint64(100)
    50  	knownVote := unittest.VoteFixture(unittest.WithVoteView(view))
    51  	doubleVote := unittest.VoteFixture(unittest.WithVoteView(view), unittest.WithVoteSignerID(knownVote.SignerID))
    52  
    53  	cache := NewVotesCache(view)
    54  
    55  	// unknown vote
    56  	vote, found := cache.GetVote(unittest.IdentifierFixture())
    57  	assert.Nil(t, vote)
    58  	assert.False(t, found)
    59  
    60  	// known vote
    61  	err := cache.AddVote(knownVote)
    62  	assert.NoError(t, err)
    63  	vote, found = cache.GetVote(knownVote.SignerID)
    64  	assert.Equal(t, knownVote, vote)
    65  	assert.True(t, found)
    66  
    67  	// for a signer ID with a known vote, the cache should memorize the _first_ encountered vote
    68  	err = cache.AddVote(doubleVote)
    69  	assert.True(t, model.IsDoubleVoteError(err))
    70  	vote, found = cache.GetVote(doubleVote.SignerID)
    71  	assert.Equal(t, knownVote, vote)
    72  	assert.True(t, found)
    73  }
    74  
    75  // TestVotesCache_All tests that All returns previously added votes in same order
    76  func TestVotesCache_All(t *testing.T) {
    77  	t.Parallel()
    78  
    79  	view := uint64(100)
    80  	cache := NewVotesCache(view)
    81  	expectedVotes := make([]*model.Vote, 0, 5)
    82  	for i := range expectedVotes {
    83  		vote := unittest.VoteFixture(unittest.WithVoteView(view))
    84  		expectedVotes[i] = vote
    85  		require.NoError(t, cache.AddVote(vote))
    86  	}
    87  	require.Equal(t, expectedVotes, cache.All())
    88  }
    89  
    90  // TestVotesCache_RegisterVoteConsumer tests that registered vote consumer receives all previously added votes as well as
    91  // new ones in expected order.
    92  func TestVotesCache_RegisterVoteConsumer(t *testing.T) {
    93  	t.Parallel()
    94  
    95  	view := uint64(100)
    96  	cache := NewVotesCache(view)
    97  	votesBatchSize := 5
    98  	expectedVotes := make([]*model.Vote, 0, votesBatchSize)
    99  	// produce first batch before registering vote consumer
   100  	for i := range expectedVotes {
   101  		vote := unittest.VoteFixture(unittest.WithVoteView(view))
   102  		expectedVotes[i] = vote
   103  		require.NoError(t, cache.AddVote(vote))
   104  	}
   105  
   106  	consumedVotes := make([]*model.Vote, 0)
   107  	voteConsumer := func(vote *model.Vote) {
   108  		consumedVotes = append(consumedVotes, vote)
   109  	}
   110  
   111  	// registering vote consumer has to fill consumedVotes using callback
   112  	cache.RegisterVoteConsumer(voteConsumer)
   113  	// all cached votes should be fed into the consumer right away
   114  	require.Equal(t, expectedVotes, consumedVotes)
   115  
   116  	// produce second batch after registering vote consumer
   117  	for i := 0; i < votesBatchSize; i++ {
   118  		vote := unittest.VoteFixture(unittest.WithVoteView(view))
   119  		expectedVotes = append(expectedVotes, vote)
   120  		require.NoError(t, cache.AddVote(vote))
   121  	}
   122  
   123  	// at this point consumedVotes has to have all votes created before and after registering vote
   124  	// consumer, and they must be in same order
   125  	require.Equal(t, expectedVotes, consumedVotes)
   126  }
   127  
   128  // BenchmarkAdd measured the time it takes to add `numberVotes` concurrently to the VotesCache.
   129  // On MacBook with Intel i7-7820HQ CPU @ 2.90GHz:
   130  // adding 1 million votes in total, with 20 threads concurrently, took 0.48s
   131  func BenchmarkAdd(b *testing.B) {
   132  	numberVotes := 1_000_000
   133  	threads := 20
   134  
   135  	// Setup: create worker routines and votes to feed
   136  	view := uint64(10)
   137  	cache := NewVotesCache(view)
   138  
   139  	var start sync.WaitGroup
   140  	start.Add(threads)
   141  	var done sync.WaitGroup
   142  	done.Add(threads)
   143  
   144  	blockID := unittest.IdentifierFixture()
   145  	n := numberVotes / threads
   146  
   147  	for ; threads > 0; threads-- {
   148  		go func(i int) {
   149  			// create votes and signal ready
   150  			votes := make([]model.Vote, 0, n)
   151  			for len(votes) < n {
   152  				v := unittest.VoteFixture(unittest.WithVoteView(view), unittest.WithVoteBlockID(blockID))
   153  				votes = append(votes, *v)
   154  			}
   155  			start.Done()
   156  
   157  			// Wait for last worker routine to signal ready. Then,
   158  			// feed all votes into cache
   159  			start.Wait()
   160  			for _, v := range votes {
   161  				err := cache.AddVote(&v)
   162  				assert.NoError(b, err)
   163  			}
   164  			done.Done()
   165  		}(threads)
   166  	}
   167  	start.Wait()
   168  	t1 := time.Now()
   169  	done.Wait()
   170  	duration := time.Since(t1)
   171  	fmt.Printf("=> adding %d votes to Cache took %f seconds\n", cache.Size(), duration.Seconds())
   172  }