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 }