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  }