github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/internal/subscriptionCache_test.go (about) 1 package internal_test 2 3 import ( 4 "sync" 5 "testing" 6 "time" 7 8 "github.com/stretchr/testify/require" 9 10 "github.com/onflow/flow-go/module/metrics" 11 "github.com/onflow/flow-go/network" 12 "github.com/onflow/flow-go/network/p2p/scoring/internal" 13 "github.com/onflow/flow-go/utils/unittest" 14 ) 15 16 // TestNewSubscriptionRecordCache tests that NewSubscriptionRecordCache returns a valid cache. 17 func TestNewSubscriptionRecordCache(t *testing.T) { 18 sizeLimit := uint32(100) 19 20 cache := internal.NewSubscriptionRecordCache( 21 sizeLimit, 22 unittest.Logger(), 23 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 24 25 require.NotNil(t, cache, "cache should not be nil") 26 require.IsType(t, &internal.SubscriptionRecordCache{}, cache, "cache should be of type *SubscriptionRecordCache") 27 } 28 29 // TestSubscriptionCache_GetSubscribedTopics tests the retrieval of subscribed topics for a peer. 30 func TestSubscriptionCache_GetSubscribedTopics(t *testing.T) { 31 sizeLimit := uint32(100) 32 cache := internal.NewSubscriptionRecordCache( 33 sizeLimit, 34 unittest.Logger(), 35 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 36 37 // create a dummy peer ID 38 peerID := unittest.PeerIdFixture(t) 39 40 // case when the peer has a subscription 41 topics := []string{"topic1", "topic2"} 42 updatedTopics, err := cache.AddWithInitTopicForPeer(peerID, topics[0]) 43 require.NoError(t, err, "adding topic 1 should not produce an error") 44 require.Equal(t, topics[:1], updatedTopics, "updated topics should match the added topic") 45 updatedTopics, err = cache.AddWithInitTopicForPeer(peerID, topics[1]) 46 require.NoError(t, err, "adding topic 2 should not produce an error") 47 require.Equal(t, topics, updatedTopics, "updated topics should match the added topic") 48 49 retrievedTopics, found := cache.GetSubscribedTopics(peerID) 50 require.True(t, found, "peer should be found") 51 require.ElementsMatch(t, topics, retrievedTopics, "retrieved topics should match the added topics") 52 53 // case when the peer does not have a subscription 54 nonExistentPeerID := unittest.PeerIdFixture(t) 55 retrievedTopics, found = cache.GetSubscribedTopics(nonExistentPeerID) 56 require.False(t, found, "non-existent peer should not be found") 57 require.Nil(t, retrievedTopics, "retrieved topics for non-existent peer should be nil") 58 } 59 60 // TestSubscriptionCache_MoveToNextUpdateCycle tests the increment of update cycles in SubscriptionRecordCache. 61 // The first increment should set the cycle to 1, and the second increment should set the cycle to 2. 62 func TestSubscriptionCache_MoveToNextUpdateCycle(t *testing.T) { 63 sizeLimit := uint32(100) 64 cache := internal.NewSubscriptionRecordCache( 65 sizeLimit, 66 unittest.Logger(), 67 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 68 69 // initial cycle should be 0, so first increment sets it to 1 70 firstCycle := cache.MoveToNextUpdateCycle() 71 require.Equal(t, uint64(1), firstCycle, "first cycle should be 1 after first increment") 72 73 // increment cycle again and verify it's now 2 74 secondCycle := cache.MoveToNextUpdateCycle() 75 require.Equal(t, uint64(2), secondCycle, "second cycle should be 2 after second increment") 76 } 77 78 // TestSubscriptionCache_TestAddTopicForPeer tests adding a topic for a peer. 79 func TestSubscriptionCache_TestAddTopicForPeer(t *testing.T) { 80 sizeLimit := uint32(100) 81 cache := internal.NewSubscriptionRecordCache( 82 sizeLimit, 83 unittest.Logger(), 84 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 85 86 // case when adding a topic to an existing peer 87 existingPeerID := unittest.PeerIdFixture(t) 88 firstTopic := "topic1" 89 secondTopic := "topic2" 90 91 // add first topic to the existing peer 92 _, err := cache.AddWithInitTopicForPeer(existingPeerID, firstTopic) 93 require.NoError(t, err, "adding first topic to existing peer should not produce an error") 94 95 // add second topic to the same peer 96 updatedTopics, err := cache.AddWithInitTopicForPeer(existingPeerID, secondTopic) 97 require.NoError(t, err, "adding second topic to existing peer should not produce an error") 98 require.ElementsMatch(t, []string{firstTopic, secondTopic}, updatedTopics, "updated topics should match the added topics") 99 100 // case when adding a topic to a new peer 101 newPeerID := unittest.PeerIdFixture(t) 102 newTopic := "newTopic" 103 104 // add a topic to the new peer 105 updatedTopics, err = cache.AddWithInitTopicForPeer(newPeerID, newTopic) 106 require.NoError(t, err, "adding topic to new peer should not produce an error") 107 require.Equal(t, []string{newTopic}, updatedTopics, "updated topics for new peer should match the added topic") 108 109 // sanity check that the topics for existing peer are still the same 110 retrievedTopics, found := cache.GetSubscribedTopics(existingPeerID) 111 require.True(t, found, "existing peer should be found") 112 require.ElementsMatch(t, []string{firstTopic, secondTopic}, retrievedTopics, "retrieved topics should match the added topics") 113 } 114 115 // TestSubscriptionCache_DuplicateTopics tests adding a duplicate topic for a peer. The duplicate topic should not be added. 116 func TestSubscriptionCache_DuplicateTopics(t *testing.T) { 117 sizeLimit := uint32(100) 118 cache := internal.NewSubscriptionRecordCache( 119 sizeLimit, 120 unittest.Logger(), 121 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 122 123 peerID := unittest.PeerIdFixture(t) 124 topic := "topic1" 125 126 // add first topic to the existing peer 127 _, err := cache.AddWithInitTopicForPeer(peerID, topic) 128 require.NoError(t, err, "adding first topic to existing peer should not produce an error") 129 130 // add second topic to the same peer 131 updatedTopics, err := cache.AddWithInitTopicForPeer(peerID, topic) 132 require.NoError(t, err, "adding duplicate topic to existing peer should not produce an error") 133 require.Equal(t, []string{topic}, updatedTopics, "duplicate topic should not be added") 134 } 135 136 // TestSubscriptionCache_MoveUpdateCycle tests that (1) within one update cycle, "AddWithInitTopicForPeer" calls append the topics to the list of 137 // subscribed topics for peer, (2) as long as there is no "AddWithInitTopicForPeer" call, moving to the next update cycle 138 // does not change the subscribed topics for a peer, and (3) calling "AddWithInitTopicForPeer" after moving to the next update 139 // cycle clears the subscribed topics for a peer and adds the new topic. 140 func TestSubscriptionCache_MoveUpdateCycle(t *testing.T) { 141 sizeLimit := uint32(100) 142 cache := internal.NewSubscriptionRecordCache( 143 sizeLimit, 144 unittest.Logger(), 145 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 146 147 peerID := unittest.PeerIdFixture(t) 148 topic1 := "topic1" 149 topic2 := "topic2" 150 topic3 := "topic3" 151 topic4 := "topic4" 152 153 // adds topic1, topic2, and topic3 to the peer 154 topics, err := cache.AddWithInitTopicForPeer(peerID, topic1) 155 require.NoError(t, err, "adding first topic to existing peer should not produce an error") 156 require.Equal(t, []string{topic1}, topics, "updated topics should match the added topic") 157 topics, err = cache.AddWithInitTopicForPeer(peerID, topic2) 158 require.NoError(t, err, "adding second topic to existing peer should not produce an error") 159 require.Equal(t, []string{topic1, topic2}, topics, "updated topics should match the added topics") 160 topics, err = cache.AddWithInitTopicForPeer(peerID, topic3) 161 require.NoError(t, err, "adding third topic to existing peer should not produce an error") 162 require.Equal(t, []string{topic1, topic2, topic3}, topics, "updated topics should match the added topics") 163 164 // move to next update cycle 165 cache.MoveToNextUpdateCycle() 166 topics, found := cache.GetSubscribedTopics(peerID) 167 require.True(t, found, "existing peer should be found") 168 require.ElementsMatch(t, []string{topic1, topic2, topic3}, topics, "retrieved topics should match the added topics") 169 170 // add topic4 to the peer; since we moved to the next update cycle, the topics for the peer should be cleared 171 // and topic4 should be the only topic for the peer 172 topics, err = cache.AddWithInitTopicForPeer(peerID, topic4) 173 require.NoError(t, err, "adding fourth topic to existing peer should not produce an error") 174 require.Equal(t, []string{topic4}, topics, "updated topics should match the added topic") 175 176 // move to next update cycle 177 cache.MoveToNextUpdateCycle() 178 179 // since we did not add any topic to the peer, the topics for the peer should be the same as before 180 topics, found = cache.GetSubscribedTopics(peerID) 181 require.True(t, found, "existing peer should be found") 182 require.ElementsMatch(t, []string{topic4}, topics, "retrieved topics should match the added topics") 183 } 184 185 // TestSubscriptionCache_MoveUpdateCycleWithDifferentPeers tests that moving to the next update cycle does not affect the subscribed 186 // topics for other peers. 187 func TestSubscriptionCache_MoveUpdateCycleWithDifferentPeers(t *testing.T) { 188 sizeLimit := uint32(100) 189 cache := internal.NewSubscriptionRecordCache( 190 sizeLimit, 191 unittest.Logger(), 192 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 193 194 peer1 := unittest.PeerIdFixture(t) 195 peer2 := unittest.PeerIdFixture(t) 196 topic1 := "topic1" 197 topic2 := "topic2" 198 199 // add topic1 to peer1 200 topics, err := cache.AddWithInitTopicForPeer(peer1, topic1) 201 require.NoError(t, err, "adding first topic to peer1 should not produce an error") 202 require.Equal(t, []string{topic1}, topics, "updated topics should match the added topic") 203 204 // add topic2 to peer2 205 topics, err = cache.AddWithInitTopicForPeer(peer2, topic2) 206 require.NoError(t, err, "adding first topic to peer2 should not produce an error") 207 require.Equal(t, []string{topic2}, topics, "updated topics should match the added topic") 208 209 // move to next update cycle 210 cache.MoveToNextUpdateCycle() 211 212 // since we did not add any topic to the peers, the topics for the peers should be the same as before 213 topics, found := cache.GetSubscribedTopics(peer1) 214 require.True(t, found, "peer1 should be found") 215 require.ElementsMatch(t, []string{topic1}, topics, "retrieved topics should match the added topics") 216 217 topics, found = cache.GetSubscribedTopics(peer2) 218 require.True(t, found, "peer2 should be found") 219 require.ElementsMatch(t, []string{topic2}, topics, "retrieved topics should match the added topics") 220 221 // now add topic2 to peer1; it should overwrite the previous topics for peer1, but not affect the topics for peer2 222 topics, err = cache.AddWithInitTopicForPeer(peer1, topic2) 223 require.NoError(t, err, "adding second topic to peer1 should not produce an error") 224 require.Equal(t, []string{topic2}, topics, "updated topics should match the added topic") 225 226 topics, found = cache.GetSubscribedTopics(peer2) 227 require.True(t, found, "peer2 should be found") 228 require.ElementsMatch(t, []string{topic2}, topics, "retrieved topics should match the added topics") 229 } 230 231 // TestSubscriptionCache_ConcurrentUpdate tests subscription cache update in a concurrent environment. 232 func TestSubscriptionCache_ConcurrentUpdate(t *testing.T) { 233 sizeLimit := uint32(100) 234 cache := internal.NewSubscriptionRecordCache( 235 sizeLimit, 236 unittest.Logger(), 237 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 238 239 peerIds := unittest.PeerIdFixtures(t, 100) 240 topics := []string{"topic1", "topic2", "topic3"} 241 242 allUpdatesDone := sync.WaitGroup{} 243 for _, pid := range peerIds { 244 for _, topic := range topics { 245 pid := pid 246 topic := topic 247 allUpdatesDone.Add(1) 248 go func() { 249 defer allUpdatesDone.Done() 250 _, err := cache.AddWithInitTopicForPeer(pid, topic) 251 require.NoError(t, err, "adding topic to peer should not produce an error") 252 }() 253 } 254 } 255 256 unittest.RequireReturnsBefore(t, allUpdatesDone.Wait, 1*time.Second, "all updates did not finish in time") 257 258 // verify that all peers have all topics; concurrently 259 allTopicsVerified := sync.WaitGroup{} 260 for _, pid := range peerIds { 261 pid := pid 262 allTopicsVerified.Add(1) 263 go func() { 264 defer allTopicsVerified.Done() 265 topics, found := cache.GetSubscribedTopics(pid) 266 require.True(t, found, "peer should be found") 267 require.ElementsMatch(t, topics, topics, "retrieved topics should match the added topics") 268 }() 269 } 270 271 unittest.RequireReturnsBefore(t, allTopicsVerified.Wait, 1*time.Second, "all topics were not verified in time") 272 } 273 274 // TestSubscriptionCache_TestSizeLimit tests that the cache evicts the least recently used peer when the cache size limit is reached. 275 func TestSubscriptionCache_TestSizeLimit(t *testing.T) { 276 sizeLimit := uint32(100) 277 cache := internal.NewSubscriptionRecordCache( 278 sizeLimit, 279 unittest.Logger(), 280 metrics.NewSubscriptionRecordCacheMetricsFactory(metrics.NewNoopHeroCacheMetricsFactory(), network.PrivateNetwork)) 281 282 peerIds := unittest.PeerIdFixtures(t, 100) 283 topics := []string{"topic1", "topic2", "topic3"} 284 285 // add topics to peers 286 for _, pid := range peerIds { 287 for _, topic := range topics { 288 _, err := cache.AddWithInitTopicForPeer(pid, topic) 289 require.NoError(t, err, "adding topic to peer should not produce an error") 290 } 291 } 292 293 // verify that all peers have all topics 294 for _, pid := range peerIds { 295 topics, found := cache.GetSubscribedTopics(pid) 296 require.True(t, found, "peer should be found") 297 require.ElementsMatch(t, topics, topics, "retrieved topics should match the added topics") 298 } 299 300 // add one more peer and verify that the first peer is evicted 301 newPeerID := unittest.PeerIdFixture(t) 302 _, err := cache.AddWithInitTopicForPeer(newPeerID, topics[0]) 303 require.NoError(t, err, "adding topic to peer should not produce an error") 304 305 _, found := cache.GetSubscribedTopics(peerIds[0]) 306 require.False(t, found, "peer should not be found") 307 308 // verify that all other peers still have all topics 309 for _, pid := range peerIds[1:] { 310 topics, found := cache.GetSubscribedTopics(pid) 311 require.True(t, found, "peer should be found") 312 require.ElementsMatch(t, topics, topics, "retrieved topics should match the added topics") 313 } 314 315 // verify that the new peer has the topic 316 topics, found = cache.GetSubscribedTopics(newPeerID) 317 require.True(t, found, "peer should be found") 318 require.ElementsMatch(t, topics, topics, "retrieved topics should match the added topics") 319 }