github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/inspector/internal/cache/cache_test.go (about)

     1  package cache
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/libp2p/go-libp2p/core/peer"
    10  	"github.com/rs/zerolog"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/onflow/flow-go/model/flow"
    14  	"github.com/onflow/flow-go/module"
    15  	"github.com/onflow/flow-go/module/metrics"
    16  	"github.com/onflow/flow-go/utils/unittest"
    17  )
    18  
    19  const defaultDecay = 0.99
    20  
    21  // TestRecordCache_Init tests the Init method of the RecordCache.
    22  // It ensures that the method returns true when a new record is initialized
    23  // and false when an existing record is initialized.
    24  func TestRecordCache_Init(t *testing.T) {
    25  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
    26  
    27  	peerID1 := unittest.PeerIdFixture(t)
    28  	peerID2 := unittest.PeerIdFixture(t)
    29  
    30  	// test initializing a record for an node ID that doesn't exist in the cache
    31  	gauge, ok, err := cache.GetWithInit(peerID1)
    32  	require.NoError(t, err)
    33  	require.True(t, ok, "expected record to exist")
    34  	require.Zerof(t, gauge, "expected gauge to be 0")
    35  	require.Equal(t, uint(1), cache.Size(), "expected cache to have one additional record")
    36  
    37  	// test initializing a record for an node ID that already exists in the cache
    38  	gaugeAgain, ok, err := cache.GetWithInit(peerID1)
    39  	require.NoError(t, err)
    40  	require.True(t, ok, "expected record to still exist")
    41  	require.Zerof(t, gaugeAgain, "expected same gauge to be 0")
    42  	require.Equal(t, gauge, gaugeAgain, "expected records to be the same")
    43  	require.Equal(t, uint(1), cache.Size(), "expected cache to still have one additional record")
    44  
    45  	// test initializing a record for another node ID
    46  	gauge2, ok, err := cache.GetWithInit(peerID2)
    47  	require.NoError(t, err)
    48  	require.True(t, ok, "expected record to exist")
    49  	require.Zerof(t, gauge2, "expected second gauge to be 0")
    50  	require.Equal(t, uint(2), cache.Size(), "expected cache to have two additional records")
    51  }
    52  
    53  // TestRecordCache_ConcurrentInit tests the concurrent initialization of records.
    54  // The test covers the following scenarios:
    55  // 1. Multiple goroutines initializing records for different node IDs.
    56  // 2. Ensuring that all records are correctly initialized.
    57  func TestRecordCache_ConcurrentInit(t *testing.T) {
    58  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
    59  
    60  	pids := unittest.PeerIdFixtures(t, 10)
    61  
    62  	var wg sync.WaitGroup
    63  	wg.Add(len(pids))
    64  
    65  	for _, pid := range pids {
    66  		go func(id peer.ID) {
    67  			defer wg.Done()
    68  			gauge, found, err := cache.GetWithInit(id)
    69  			require.NoError(t, err)
    70  			require.True(t, found)
    71  			require.Zerof(t, gauge, "expected all gauge values to be initialized to 0")
    72  		}(pid)
    73  	}
    74  
    75  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
    76  }
    77  
    78  // TestRecordCache_ConcurrentSameRecordInit tests the concurrent initialization of the same record.
    79  // The test covers the following scenarios:
    80  // 1. Multiple goroutines attempting to initialize the same record concurrently.
    81  // 2. Only one goroutine successfully initializes the record, and others receive false on initialization.
    82  // 3. The record is correctly initialized in the cache and can be retrieved using the GetWithInit method.
    83  func TestRecordCache_ConcurrentSameRecordInit(t *testing.T) {
    84  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
    85  
    86  	nodeID := unittest.PeerIdFixture(t)
    87  	const concurrentAttempts = 10
    88  
    89  	var wg sync.WaitGroup
    90  	wg.Add(concurrentAttempts)
    91  
    92  	for i := 0; i < concurrentAttempts; i++ {
    93  		go func() {
    94  			defer wg.Done()
    95  			gauge, found, err := cache.GetWithInit(nodeID)
    96  			require.NoError(t, err)
    97  			require.True(t, found)
    98  			require.Zero(t, gauge)
    99  		}()
   100  	}
   101  
   102  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   103  
   104  	// ensure that only one goroutine successfully initialized the record
   105  	require.Equal(t, uint(1), cache.Size())
   106  }
   107  
   108  // TestRecordCache_ReceivedClusterPrefixedMessage tests the ReceivedClusterPrefixedMessage method of the RecordCache.
   109  // The test covers the following scenarios:
   110  // 1. Updating a record gauge for an existing node ID.
   111  // 2. Attempting to update a record gauge  for a non-existing node ID should not result in error. ReceivedClusterPrefixedMessage should always attempt to initialize the gauge.
   112  // 3. Multiple updates on the same record only initialize the record once.
   113  func TestRecordCache_ReceivedClusterPrefixedMessage(t *testing.T) {
   114  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   115  
   116  	peerID1 := unittest.PeerIdFixture(t)
   117  	peerID2 := unittest.PeerIdFixture(t)
   118  
   119  	gauge, err := cache.ReceivedClusterPrefixedMessage(peerID1)
   120  	require.NoError(t, err)
   121  	require.Equal(t, float64(1), gauge)
   122  
   123  	// get will apply a slightl decay resulting
   124  	// in a gauge value less than gauge which is 1 but greater than 0.9
   125  	currentGauge, ok, err := cache.GetWithInit(peerID1)
   126  	require.NoError(t, err)
   127  	require.True(t, ok)
   128  	require.LessOrEqual(t, currentGauge, gauge)
   129  	require.Greater(t, currentGauge, 0.9)
   130  
   131  	_, ok, err = cache.GetWithInit(peerID2)
   132  	require.NoError(t, err)
   133  	require.True(t, ok)
   134  
   135  	// test adjusting the spam record for a non-existing node ID
   136  	peerID3 := unittest.PeerIdFixture(t)
   137  	gauge3, err := cache.ReceivedClusterPrefixedMessage(peerID3)
   138  	require.NoError(t, err)
   139  	require.Equal(t, float64(1), gauge3)
   140  
   141  	// when updated the value should be incremented from 1 -> 2 and slightly decayed resulting
   142  	// in a gauge value less than 2 but greater than 1.9
   143  	gauge3, err = cache.ReceivedClusterPrefixedMessage(peerID3)
   144  	require.NoError(t, err)
   145  	require.LessOrEqual(t, gauge3, 2.0)
   146  	require.Greater(t, gauge3, 1.9)
   147  }
   148  
   149  // TestRecordCache_UpdateDecay ensures that a gauge in the record cache is eventually decayed back to 0 after some time.
   150  func TestRecordCache_Decay(t *testing.T) {
   151  	cache := cacheFixture(t, 100, 0.09, zerolog.Nop(), metrics.NewNoopCollector())
   152  
   153  	peerID1 := unittest.PeerIdFixture(t)
   154  
   155  	// initialize spam records for peerID1 and peerID2
   156  	gauge, err := cache.ReceivedClusterPrefixedMessage(peerID1)
   157  	require.Equal(t, float64(1), gauge)
   158  	require.NoError(t, err)
   159  	gauge, ok, err := cache.GetWithInit(peerID1)
   160  	require.True(t, ok)
   161  	require.NoError(t, err)
   162  	// gauge should have been delayed slightly
   163  	require.True(t, gauge < float64(1))
   164  
   165  	time.Sleep(time.Second)
   166  
   167  	gauge, ok, err = cache.GetWithInit(peerID1)
   168  	require.True(t, ok)
   169  	require.NoError(t, err)
   170  	// gauge should have been delayed slightly, but closer to 0
   171  	require.Less(t, gauge, 0.1)
   172  }
   173  
   174  // TestRecordCache_Identities tests the NodeIDs method of the RecordCache.
   175  // The test covers the following scenarios:
   176  // 1. Initializing the cache with multiple records.
   177  // 2. Checking if the NodeIDs method returns the correct set of node IDs.
   178  func TestRecordCache_Identities(t *testing.T) {
   179  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   180  
   181  	// initialize spam records for a few node IDs
   182  	peerID1 := unittest.PeerIdFixture(t)
   183  	peerID2 := unittest.PeerIdFixture(t)
   184  	peerID3 := unittest.PeerIdFixture(t)
   185  
   186  	_, ok, err := cache.GetWithInit(peerID1)
   187  	require.NoError(t, err)
   188  	require.True(t, ok)
   189  	_, ok, err = cache.GetWithInit(peerID2)
   190  	require.NoError(t, err)
   191  	require.True(t, ok)
   192  	_, ok, err = cache.GetWithInit(peerID3)
   193  	require.NoError(t, err)
   194  	require.True(t, ok)
   195  
   196  	// check if the NodeIDs method returns the correct set of node IDs
   197  	identities := cache.NodeIDs()
   198  	require.Equal(t, 3, len(identities))
   199  	require.ElementsMatch(t, identities, []flow.Identifier{cache.MakeId(peerID1), cache.MakeId(peerID2), cache.MakeId(peerID3)})
   200  }
   201  
   202  // TestRecordCache_Remove tests the Remove method of the RecordCache.
   203  // The test covers the following scenarios:
   204  // 1. Initializing the cache with multiple records.
   205  // 2. Removing a record and checking if it is removed correctly.
   206  // 3. Ensuring the other records are still in the cache after removal.
   207  // 4. Attempting to remove a non-existent node ID.
   208  func TestRecordCache_Remove(t *testing.T) {
   209  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   210  
   211  	// initialize spam records for a few node IDs
   212  	peerID1 := unittest.PeerIdFixture(t)
   213  	peerID2 := unittest.PeerIdFixture(t)
   214  	peerID3 := unittest.PeerIdFixture(t)
   215  
   216  	_, ok, err := cache.GetWithInit(peerID1)
   217  	require.NoError(t, err)
   218  	require.True(t, ok)
   219  	_, ok, err = cache.GetWithInit(peerID2)
   220  	require.NoError(t, err)
   221  	require.True(t, ok)
   222  	_, ok, err = cache.GetWithInit(peerID3)
   223  	require.NoError(t, err)
   224  	require.True(t, ok)
   225  
   226  	numOfIds := uint(3)
   227  	require.Equal(t, numOfIds, cache.Size(), fmt.Sprintf("expected size of the cache to be %d", numOfIds))
   228  	// remove peerID1 and check if the record is removed
   229  	require.True(t, cache.Remove(peerID1))
   230  	require.NotContains(t, peerID1, cache.NodeIDs())
   231  
   232  	// check if the other node IDs are still in the cache
   233  	_, exists, err := cache.GetWithInit(peerID2)
   234  	require.NoError(t, err)
   235  	require.True(t, exists)
   236  	_, exists, err = cache.GetWithInit(peerID3)
   237  	require.NoError(t, err)
   238  	require.True(t, exists)
   239  
   240  	// attempt to remove a non-existent node ID
   241  	nodeID4 := unittest.PeerIdFixture(t)
   242  	require.False(t, cache.Remove(nodeID4))
   243  }
   244  
   245  // TestRecordCache_ConcurrentRemove tests the concurrent removal of records for different node IDs.
   246  // The test covers the following scenarios:
   247  // 1. Multiple goroutines removing records for different node IDs concurrently.
   248  // 2. The records are correctly removed from the cache.
   249  func TestRecordCache_ConcurrentRemove(t *testing.T) {
   250  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   251  
   252  	peerIds := unittest.PeerIdFixtures(t, 10)
   253  	for _, pid := range peerIds {
   254  		_, ok, err := cache.GetWithInit(pid)
   255  		require.NoError(t, err)
   256  		require.True(t, ok)
   257  	}
   258  
   259  	var wg sync.WaitGroup
   260  	wg.Add(len(peerIds))
   261  
   262  	for _, pid := range peerIds {
   263  		go func(id peer.ID) {
   264  			defer wg.Done()
   265  			removed := cache.Remove(id)
   266  			require.True(t, removed)
   267  			require.NotContains(t, id, cache.NodeIDs())
   268  		}(pid)
   269  	}
   270  
   271  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   272  
   273  	require.Equal(t, uint(0), cache.Size())
   274  }
   275  
   276  // TestRecordCache_ConcurrentUpdatesAndReads tests the concurrent adjustments and reads of records for different
   277  // node IDs. The test covers the following scenarios:
   278  // 1. Multiple goroutines adjusting records for different node IDs concurrently.
   279  // 2. Multiple goroutines getting records for different node IDs concurrently.
   280  // 3. The adjusted records are correctly updated in the cache.
   281  func TestRecordCache_ConcurrentUpdatesAndReads(t *testing.T) {
   282  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   283  
   284  	peerIds := unittest.PeerIdFixtures(t, 10)
   285  	for _, pid := range peerIds {
   286  		_, ok, err := cache.GetWithInit(pid)
   287  		require.NoError(t, err)
   288  		require.True(t, ok)
   289  	}
   290  
   291  	var wg sync.WaitGroup
   292  	wg.Add(len(peerIds) * 2)
   293  
   294  	for _, pid := range peerIds {
   295  		// adjust spam records concurrently
   296  		go func(id peer.ID) {
   297  			defer wg.Done()
   298  			_, err := cache.ReceivedClusterPrefixedMessage(id)
   299  			require.NoError(t, err)
   300  		}(pid)
   301  
   302  		// get spam records concurrently
   303  		go func(id peer.ID) {
   304  			defer wg.Done()
   305  			_, found, err := cache.GetWithInit(id)
   306  			require.NoError(t, err)
   307  			require.True(t, found)
   308  		}(pid)
   309  	}
   310  
   311  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   312  
   313  	// ensure that the records are correctly updated in the cache
   314  	for _, pid := range peerIds {
   315  		gauge, found, err := cache.GetWithInit(pid)
   316  		require.NoError(t, err)
   317  		require.True(t, found)
   318  		// slight decay will result in 0.9 < gauge < 1
   319  		require.LessOrEqual(t, gauge, 1.0)
   320  		require.Greater(t, gauge, 0.9)
   321  	}
   322  }
   323  
   324  // TestRecordCache_ConcurrentInitAndRemove tests the concurrent initialization and removal of records for different
   325  // node IDs. The test covers the following scenarios:
   326  // 1. Multiple goroutines initializing records for different node IDs concurrently.
   327  // 2. Multiple goroutines removing records for different node IDs concurrently.
   328  // 3. The initialized records are correctly added to the cache.
   329  // 4. The removed records are correctly removed from the cache.
   330  func TestRecordCache_ConcurrentInitAndRemove(t *testing.T) {
   331  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   332  
   333  	peerIds := unittest.PeerIdFixtures(t, 20)
   334  	peerIdsToAdd := peerIds[:10]
   335  	peerIdsToRemove := peerIds[10:]
   336  
   337  	for _, pid := range peerIdsToRemove {
   338  		_, ok, err := cache.GetWithInit(pid)
   339  		require.NoError(t, err)
   340  		require.True(t, ok)
   341  	}
   342  
   343  	var wg sync.WaitGroup
   344  	wg.Add(len(peerIds))
   345  
   346  	// initialize spam records concurrently
   347  	for _, pid := range peerIdsToAdd {
   348  		go func(id peer.ID) {
   349  			defer wg.Done()
   350  			_, ok, err := cache.GetWithInit(id)
   351  			require.NoError(t, err)
   352  			require.True(t, ok)
   353  		}(pid)
   354  	}
   355  
   356  	// remove spam records concurrently
   357  	for _, pid := range peerIdsToRemove {
   358  		go func(id peer.ID) {
   359  			defer wg.Done()
   360  			cache.Remove(id)
   361  			require.NotContains(t, id, cache.NodeIDs())
   362  		}(pid)
   363  	}
   364  
   365  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   366  
   367  	// ensure that the initialized records are correctly added to the cache
   368  	// and removed records are correctly removed from the cache
   369  	expectedIds := make([]flow.Identifier, len(peerIdsToAdd))
   370  	for i, pid := range peerIdsToAdd {
   371  		expectedIds[i] = cache.MakeId(pid)
   372  	}
   373  	require.ElementsMatch(t, expectedIds, cache.NodeIDs())
   374  }
   375  
   376  // TestRecordCache_ConcurrentInitRemoveUpdate tests the concurrent initialization, removal, and adjustment of
   377  // records for different node IDs. The test covers the following scenarios:
   378  // 1. Multiple goroutines initializing records for different node IDs concurrently.
   379  // 2. Multiple goroutines removing records for different node IDs concurrently.
   380  // 3. Multiple goroutines adjusting records for different node IDs concurrently.
   381  func TestRecordCache_ConcurrentInitRemoveUpdate(t *testing.T) {
   382  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   383  
   384  	peerIds := unittest.PeerIdFixtures(t, 30)
   385  	peerIdsToAdd := peerIds[:10]
   386  	peerIdsToRemove := peerIds[10:20]
   387  	peerIdsToAdjust := peerIds[20:]
   388  
   389  	for _, pid := range peerIdsToRemove {
   390  		_, ok, err := cache.GetWithInit(pid)
   391  		require.NoError(t, err)
   392  		require.True(t, ok)
   393  	}
   394  
   395  	var wg sync.WaitGroup
   396  	wg.Add(len(peerIds))
   397  
   398  	// Initialize spam records concurrently
   399  	for _, pid := range peerIdsToAdd {
   400  		go func(id peer.ID) {
   401  			defer wg.Done()
   402  			_, ok, err := cache.GetWithInit(id)
   403  			require.NoError(t, err)
   404  			require.True(t, ok)
   405  		}(pid)
   406  	}
   407  
   408  	// Remove spam records concurrently
   409  	for _, pid := range peerIdsToRemove {
   410  		go func(id peer.ID) {
   411  			defer wg.Done()
   412  			cache.Remove(id)
   413  			require.NotContains(t, id, cache.NodeIDs())
   414  		}(pid)
   415  	}
   416  
   417  	// Adjust spam records concurrently
   418  	for _, pid := range peerIdsToAdjust {
   419  		go func(id peer.ID) {
   420  			defer wg.Done()
   421  			_, _ = cache.ReceivedClusterPrefixedMessage(id)
   422  		}(pid)
   423  	}
   424  
   425  	expectedPeerIds := append(peerIdsToAdd, peerIdsToAdjust...)
   426  	expectedIds := make([]flow.Identifier, len(expectedPeerIds))
   427  	for i, pid := range expectedPeerIds {
   428  		expectedIds[i] = cache.MakeId(pid)
   429  	}
   430  	unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   431  	require.ElementsMatch(t, expectedIds, cache.NodeIDs())
   432  }
   433  
   434  // TestRecordCache_EdgeCasesAndInvalidInputs tests the edge cases and invalid inputs for RecordCache methods.
   435  // The test covers the following scenarios:
   436  // 1. Initializing a record multiple times.
   437  // 2. Adjusting a non-existent record.
   438  // 3. Removing a record multiple times.
   439  func TestRecordCache_EdgeCasesAndInvalidInputs(t *testing.T) {
   440  	cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector())
   441  
   442  	peerIds := unittest.PeerIdFixtures(t, 20)
   443  	peerIdsToAdd := peerIds[:10]
   444  	peerIdsToRemove := peerIds[10:20]
   445  
   446  	for _, pid := range peerIdsToRemove {
   447  		_, ok, err := cache.GetWithInit(pid)
   448  		require.NoError(t, err)
   449  		require.True(t, ok)
   450  	}
   451  
   452  	var wg sync.WaitGroup
   453  	wg.Add(len(peerIds) + 10)
   454  
   455  	// initialize spam records concurrently
   456  	for _, pid := range peerIdsToAdd {
   457  		go func(id peer.ID) {
   458  			defer wg.Done()
   459  			retrieved, ok, err := cache.GetWithInit(id)
   460  			require.NoError(t, err)
   461  			require.True(t, ok)
   462  			require.Zero(t, retrieved)
   463  		}(pid)
   464  	}
   465  
   466  	// remove spam records concurrently
   467  	for _, pid := range peerIdsToRemove {
   468  		go func(id peer.ID) {
   469  			defer wg.Done()
   470  			require.True(t, cache.Remove(id))
   471  			require.NotContains(t, cache.MakeId(id), cache.NodeIDs())
   472  		}(pid)
   473  	}
   474  
   475  	expectedIds := make([]flow.Identifier, len(peerIds))
   476  	for i, pid := range peerIds {
   477  		expectedIds[i] = cache.MakeId(pid)
   478  	}
   479  	// call NodeIDs method concurrently
   480  	for i := 0; i < 10; i++ {
   481  		go func() {
   482  			defer wg.Done()
   483  			ids := cache.NodeIDs()
   484  			// the number of returned IDs should be less than or equal to the number of node IDs
   485  			require.True(t, len(ids) <= len(peerIds))
   486  			// the returned IDs should be a subset of the node IDs
   487  			for _, id := range ids {
   488  				require.Contains(t, expectedIds, id)
   489  			}
   490  		}()
   491  	}
   492  	unittest.RequireReturnsBefore(t, wg.Wait, 1*time.Second, "timed out waiting for goroutines to finish")
   493  }
   494  
   495  // recordFixture creates a new record entity with the given node id.
   496  // Args:
   497  // - id: the node id of the record.
   498  // Returns:
   499  // - RecordEntity: the created record entity.
   500  func recordEntityFixture(id flow.Identifier) ClusterPrefixedMessagesReceivedRecord {
   501  	return ClusterPrefixedMessagesReceivedRecord{NodeID: id, Gauge: 0.0, lastUpdated: time.Now()}
   502  }
   503  
   504  // cacheFixture returns a new *RecordCache.
   505  func cacheFixture(t *testing.T, sizeLimit uint32, recordDecay float64, logger zerolog.Logger, collector module.HeroCacheMetrics) *RecordCache {
   506  	recordFactory := func(id flow.Identifier) ClusterPrefixedMessagesReceivedRecord {
   507  		return recordEntityFixture(id)
   508  	}
   509  	config := &RecordCacheConfig{
   510  		sizeLimit:   sizeLimit,
   511  		logger:      logger,
   512  		collector:   collector,
   513  		recordDecay: recordDecay,
   514  	}
   515  	r, err := NewRecordCache(config, recordFactory)
   516  	require.NoError(t, err)
   517  	// expect cache to be empty
   518  	require.Equalf(t, uint(0), r.Size(), "cache size must be 0")
   519  	require.NotNil(t, r)
   520  	return r
   521  }