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 }