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

     1  package unicastcache_test
     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/module/metrics"
    14  	"github.com/onflow/flow-go/network/p2p/unicast"
    15  	unicastcache "github.com/onflow/flow-go/network/p2p/unicast/cache"
    16  	"github.com/onflow/flow-go/utils/unittest"
    17  )
    18  
    19  // TestNewUnicastConfigCache tests the creation of a new UnicastConfigCache.
    20  // It asserts that the cache is created and its size is 0.
    21  func TestNewUnicastConfigCache(t *testing.T) {
    22  	sizeLimit := uint32(100)
    23  	logger := zerolog.Nop()
    24  	collector := metrics.NewNoopCollector()
    25  	cache := unicastcache.NewUnicastConfigCache(sizeLimit, logger, collector, unicastConfigFixture)
    26  	require.NotNil(t, cache)
    27  	require.Equalf(t, uint(0), cache.Size(), "cache size must be 0")
    28  }
    29  
    30  // unicastConfigFixture returns a unicast config fixture.
    31  // The unicast config is initialized with the default values.
    32  func unicastConfigFixture() unicast.Config {
    33  	return unicast.Config{
    34  		StreamCreationRetryAttemptBudget: 3,
    35  	}
    36  }
    37  
    38  // TestUnicastConfigCache_Adjust tests the Adjust method of the UnicastConfigCache. It asserts that the unicast config is initialized, adjusted,
    39  // and stored in the cache.
    40  func TestUnicastConfigCache_Adjust_Init(t *testing.T) {
    41  	sizeLimit := uint32(100)
    42  	logger := zerolog.Nop()
    43  	collector := metrics.NewNoopCollector()
    44  
    45  	unicastFactoryCalled := 0
    46  	unicastConfigFactory := func() unicast.Config {
    47  		require.Less(t, unicastFactoryCalled, 2, "unicast config factory must be called at most twice")
    48  		unicastFactoryCalled++
    49  		return unicastConfigFixture()
    50  	}
    51  	adjustFuncIncrement := func(cfg unicast.Config) (unicast.Config, error) {
    52  		cfg.StreamCreationRetryAttemptBudget++
    53  		return cfg, nil
    54  	}
    55  
    56  	cache := unicastcache.NewUnicastConfigCache(sizeLimit, logger, collector, unicastConfigFactory)
    57  	require.NotNil(t, cache)
    58  	require.Zerof(t, cache.Size(), "cache size must be 0")
    59  
    60  	peerID1 := unittest.PeerIdFixture(t)
    61  	peerID2 := unittest.PeerIdFixture(t)
    62  
    63  	// Initializing the unicast config for peerID1 through GetWithInit.
    64  	// unicast config for peerID1 does not exist in the cache, so it must be initialized when using GetWithInit.
    65  	cfg, err := cache.GetWithInit(peerID1)
    66  	require.NoError(t, err)
    67  	require.NotNil(t, cfg, "unicast config must not be nil")
    68  	require.Equal(t, unicastConfigFixture(), *cfg, "unicast config must be initialized with the default values")
    69  	require.Equal(t, uint(1), cache.Size(), "cache size must be 1")
    70  
    71  	// Initializing and adjusting the unicast config for peerID2 through Adjust.
    72  	// unicast config for peerID2 does not exist in the cache, so it must be initialized when using Adjust.
    73  	cfg, err = cache.AdjustWithInit(peerID2, adjustFuncIncrement)
    74  	require.NoError(t, err)
    75  	// adjusting a non-existing unicast config must not initialize the config.
    76  	require.Equal(t, uint(2), cache.Size(), "cache size must be 2")
    77  	require.Equal(t, cfg.StreamCreationRetryAttemptBudget, unicastConfigFixture().StreamCreationRetryAttemptBudget+1, "stream backoff must be 2")
    78  
    79  	// Retrieving the unicast config of peerID2 through GetWithInit.
    80  	// retrieve the unicast config for peerID2 and assert than it is initialized with the default values; and the adjust function is applied.
    81  	cfg, err = cache.GetWithInit(peerID2)
    82  	require.NoError(t, err, "unicast config must exist in the cache")
    83  	require.NotNil(t, cfg, "unicast config must not be nil")
    84  	// retrieving an existing unicast config must not change the cache size.
    85  	require.Equal(t, uint(2), cache.Size(), "cache size must be 2")
    86  	// config should be the same as the one returned by Adjust.
    87  	require.Equal(t, cfg.StreamCreationRetryAttemptBudget, unicastConfigFixture().StreamCreationRetryAttemptBudget+1, "stream backoff must be 2")
    88  
    89  	// Adjusting the unicast config of peerID1 through Adjust.
    90  	// unicast config for peerID1 already exists in the cache, so it must be adjusted when using Adjust.
    91  	cfg, err = cache.AdjustWithInit(peerID1, adjustFuncIncrement)
    92  	require.NoError(t, err)
    93  	// adjusting an existing unicast config must not change the cache size.
    94  	require.Equal(t, uint(2), cache.Size(), "cache size must be 2")
    95  	require.Equal(t, cfg.StreamCreationRetryAttemptBudget, unicastConfigFixture().StreamCreationRetryAttemptBudget+1, "stream backoff must be 2")
    96  
    97  	// Recurring adjustment of the unicast config of peerID1 through Adjust.
    98  	// unicast config for peerID1 already exists in the cache, so it must be adjusted when using Adjust.
    99  	cfg, err = cache.AdjustWithInit(peerID1, adjustFuncIncrement)
   100  	require.NoError(t, err)
   101  	// adjusting an existing unicast config must not change the cache size.
   102  	require.Equal(t, uint(2), cache.Size(), "cache size must be 2")
   103  	require.Equal(t, cfg.StreamCreationRetryAttemptBudget, unicastConfigFixture().StreamCreationRetryAttemptBudget+2, "stream backoff must be 3")
   104  }
   105  
   106  // TestUnicastConfigCache_Adjust tests the Adjust method of the UnicastConfigCache. It asserts that the unicast config is adjusted,
   107  // and stored in the cache as expected under concurrent adjustments.
   108  func TestUnicastConfigCache_Concurrent_Adjust(t *testing.T) {
   109  	sizeLimit := uint32(100)
   110  	logger := zerolog.Nop()
   111  	collector := metrics.NewNoopCollector()
   112  
   113  	cache := unicastcache.NewUnicastConfigCache(sizeLimit, logger, collector, func() unicast.Config {
   114  		return unicast.Config{} // empty unicast config
   115  	})
   116  	require.NotNil(t, cache)
   117  	require.Zerof(t, cache.Size(), "cache size must be 0")
   118  
   119  	peerIds := make([]peer.ID, sizeLimit)
   120  	for i := 0; i < int(sizeLimit); i++ {
   121  		peerId := unittest.PeerIdFixture(t)
   122  		require.NotContainsf(t, peerIds, peerId, "peer id must be unique")
   123  		peerIds[i] = peerId
   124  	}
   125  
   126  	wg := sync.WaitGroup{}
   127  	for i := 0; i < int(sizeLimit); i++ {
   128  		// adjusts the ith unicast config for peerID i times, concurrently.
   129  		for j := 0; j < i+1; j++ {
   130  			wg.Add(1)
   131  			go func(peerId peer.ID) {
   132  				defer wg.Done()
   133  				_, err := cache.AdjustWithInit(peerId, func(cfg unicast.Config) (unicast.Config, error) {
   134  					cfg.StreamCreationRetryAttemptBudget++
   135  					return cfg, nil
   136  				})
   137  				require.NoError(t, err)
   138  			}(peerIds[i])
   139  		}
   140  	}
   141  
   142  	unittest.RequireReturnsBefore(t, wg.Wait, time.Second*1, "adjustments must be done on time")
   143  
   144  	// assert that the cache size is equal to the size limit.
   145  	require.Equal(t, uint(sizeLimit), cache.Size(), "cache size must be equal to the size limit")
   146  
   147  	// assert that the unicast config for each peer is adjusted i times, concurrently.
   148  	for i := 0; i < int(sizeLimit); i++ {
   149  		wg.Add(1)
   150  		go func(j int) {
   151  			wg.Done()
   152  
   153  			peerID := peerIds[j]
   154  			cfg, err := cache.GetWithInit(peerID)
   155  			require.NoError(t, err)
   156  			require.Equal(t,
   157  				uint64(j+1),
   158  				cfg.StreamCreationRetryAttemptBudget,
   159  				fmt.Sprintf("peerId %s unicast backoff must be adjusted %d times got: %d", peerID, j+1, cfg.StreamCreationRetryAttemptBudget))
   160  		}(i)
   161  	}
   162  
   163  	unittest.RequireReturnsBefore(t, wg.Wait, time.Second*1, "retrievals must be done on time")
   164  }
   165  
   166  // TestConcurrent_Adjust_And_Get_Is_Safe tests that concurrent adjustments and retrievals are safe, and do not cause error even if they cause eviction. The test stress tests the cache
   167  // with 2 * SizeLimit concurrent operations (SizeLimit times concurrent adjustments and SizeLimit times concurrent retrievals).
   168  // It asserts that the cache size is equal to the size limit, and the unicast config for each peer is adjusted and retrieved correctly.
   169  func TestConcurrent_Adjust_And_Get_Is_Safe(t *testing.T) {
   170  	sizeLimit := uint32(100)
   171  	logger := zerolog.Nop()
   172  	collector := metrics.NewNoopCollector()
   173  
   174  	cache := unicastcache.NewUnicastConfigCache(sizeLimit, logger, collector, unicastConfigFixture)
   175  	require.NotNil(t, cache)
   176  	require.Zerof(t, cache.Size(), "cache size must be 0")
   177  
   178  	wg := sync.WaitGroup{}
   179  	for i := 0; i < int(sizeLimit); i++ {
   180  		// concurrently adjusts the unicast configs.
   181  		wg.Add(1)
   182  		go func() {
   183  			defer wg.Done()
   184  			peerId := unittest.PeerIdFixture(t)
   185  			updatedConfig, err := cache.AdjustWithInit(peerId, func(cfg unicast.Config) (unicast.Config, error) {
   186  				cfg.StreamCreationRetryAttemptBudget = 2 // some random adjustment
   187  				cfg.ConsecutiveSuccessfulStream = 3      // some random adjustment
   188  				return cfg, nil
   189  			})
   190  			require.NoError(t, err) // concurrent adjustment must not fail.
   191  			require.Equal(t, uint64(2), updatedConfig.StreamCreationRetryAttemptBudget)
   192  			require.Equal(t, uint64(3), updatedConfig.ConsecutiveSuccessfulStream)
   193  		}()
   194  	}
   195  
   196  	// assert that the unicast config for each peer is adjusted i times, concurrently.
   197  	for i := 0; i < int(sizeLimit); i++ {
   198  		wg.Add(1)
   199  		go func() {
   200  			wg.Done()
   201  			peerId := unittest.PeerIdFixture(t)
   202  			cfg, err := cache.GetWithInit(peerId)
   203  			require.NoError(t, err) // concurrent retrieval must not fail.
   204  			require.Equal(t, unicastConfigFixture().StreamCreationRetryAttemptBudget, cfg.StreamCreationRetryAttemptBudget)
   205  			require.Equal(t, uint64(0), cfg.ConsecutiveSuccessfulStream)
   206  		}()
   207  	}
   208  
   209  	unittest.RequireReturnsBefore(t, wg.Wait, time.Second*1, "all operations must be done on time")
   210  
   211  	// cache was stress-tested with 2 * SizeLimit concurrent operations. Nevertheless, the cache size must be equal to the size limit due to LRU eviction.
   212  	require.Equal(t, uint(sizeLimit), cache.Size(), "cache size must be equal to the size limit")
   213  }
   214  
   215  // TestUnicastConfigCache_LRU_Eviction tests that the cache evicts the least recently used unicast config when the cache size reaches the size limit.
   216  func TestUnicastConfigCache_LRU_Eviction(t *testing.T) {
   217  	sizeLimit := uint32(100)
   218  	logger := zerolog.Nop()
   219  	collector := metrics.NewNoopCollector()
   220  
   221  	cache := unicastcache.NewUnicastConfigCache(sizeLimit, logger, collector, unicastConfigFixture)
   222  	require.NotNil(t, cache)
   223  	require.Zerof(t, cache.Size(), "cache size must be 0")
   224  
   225  	peerIds := make([]peer.ID, sizeLimit+1)
   226  	for i := 0; i < int(sizeLimit+1); i++ {
   227  		peerId := unittest.PeerIdFixture(t)
   228  		require.NotContainsf(t, peerIds, peerId, "peer id must be unique")
   229  		peerIds[i] = peerId
   230  	}
   231  	for i := 0; i < int(sizeLimit+1); i++ {
   232  		updatedConfig, err := cache.AdjustWithInit(peerIds[i], func(cfg unicast.Config) (unicast.Config, error) {
   233  			cfg.StreamCreationRetryAttemptBudget = 2 // some random adjustment
   234  			cfg.ConsecutiveSuccessfulStream = 3      // some random adjustment
   235  			return cfg, nil
   236  		})
   237  		require.NoError(t, err) // concurrent adjustment must not fail.
   238  		require.Equal(t, uint64(2), updatedConfig.StreamCreationRetryAttemptBudget)
   239  		require.Equal(t, uint64(3), updatedConfig.ConsecutiveSuccessfulStream)
   240  	}
   241  
   242  	// except the first peer id, all other peer ids should stay intact in the cache.
   243  	for i := 1; i < int(sizeLimit+1); i++ {
   244  		cfg, err := cache.GetWithInit(peerIds[i])
   245  		require.NoError(t, err)
   246  		require.Equal(t, uint64(2), cfg.StreamCreationRetryAttemptBudget)
   247  		require.Equal(t, uint64(3), cfg.ConsecutiveSuccessfulStream)
   248  	}
   249  
   250  	require.Equal(t, uint(sizeLimit), cache.Size(), "cache size must be equal to the size limit")
   251  
   252  	// querying the first peer id should return a fresh unicast config,
   253  	// since it should be evicted due to LRU eviction, and the initiated with the default values.
   254  	cfg, err := cache.GetWithInit(peerIds[0])
   255  	require.NoError(t, err)
   256  	require.Equal(t, unicastConfigFixture().StreamCreationRetryAttemptBudget, cfg.StreamCreationRetryAttemptBudget)
   257  	require.Equal(t, uint64(0), cfg.ConsecutiveSuccessfulStream)
   258  
   259  	require.Equal(t, uint(sizeLimit), cache.Size(), "cache size must be equal to the size limit")
   260  }