github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/stdmap/chunk_requests_test.go (about) 1 package stdmap_test 2 3 import ( 4 "math" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/require" 10 11 "github.com/onflow/flow-go/engine/verification/requester" 12 "github.com/onflow/flow-go/model/chunks" 13 "github.com/onflow/flow-go/model/flow" 14 "github.com/onflow/flow-go/model/verification" 15 "github.com/onflow/flow-go/module/mempool" 16 "github.com/onflow/flow-go/module/mempool/stdmap" 17 "github.com/onflow/flow-go/utils/unittest" 18 ) 19 20 // TestChunkRequests_UpdateRequestHistory evaluates behavior of ChuckRequests against updating request histories with 21 // different updaters. 22 func TestChunkRequests_UpdateRequestHistory(t *testing.T) { 23 qualifier := requester.RetryAfterQualifier 24 t.Run("10 chunks- 10 times incremental updater ", func(t *testing.T) { 25 incUpdater := mempool.IncrementalAttemptUpdater() 26 chunks := 10 27 expectedAttempts := 10 28 29 withUpdaterScenario(t, chunks, expectedAttempts, incUpdater, func(t *testing.T, attempts uint64, lastTried time.Time, retryAfter time.Duration) { 30 require.Equal(t, expectedAttempts, int(attempts)) // each chunk request should be attempted 10 times. 31 require.True(t, qualifier(attempts, lastTried, retryAfter)) // request should be immediately qualified for retrial. 32 }) 33 }) 34 35 t.Run("10 chunks- 10 times exponential updater", func(t *testing.T) { 36 // sets an exponential backoff updater with a maximum backoff of 1 hour, and minimum of a second. 37 minInterval := time.Second 38 maxInterval := time.Hour // intentionally is set high to avoid overflow in this test. 39 expUpdater := mempool.ExponentialUpdater(2, maxInterval, minInterval) 40 chunks := 10 41 expectedAttempts := 10 42 43 withUpdaterScenario(t, chunks, expectedAttempts, expUpdater, func(t *testing.T, attempts uint64, lastTried time.Time, 44 retryAfter time.Duration) { 45 require.Equal(t, expectedAttempts, int(attempts)) // each chunk request should be attempted 10 times. 46 47 // request should NOT be immediately qualified for retrial due to exponential backoff. 48 require.True(t, !qualifier(attempts, lastTried, retryAfter)) 49 50 // retryAfter should be equal to 2^(attempts-1) * minInterval. 51 // note that after the first attempt, retry after is set to minInterval. 52 multiplier := time.Duration(math.Pow(2, float64(expectedAttempts-1))) 53 expectedRetryAfter := minInterval * multiplier 54 require.Equal(t, expectedRetryAfter, retryAfter) 55 }) 56 }) 57 58 t.Run("10 chunks- 10 times exponential updater- underflow", func(t *testing.T) { 59 // sets an exponential backoff updater with a maximum backoff of 1 hour, and minimum of a second. 60 minInterval := time.Second 61 maxInterval := time.Hour // intentionally is set high to avoid overflow in this test. 62 // exponential multiplier is set to a very small number so that backoff always underflow, and set to 63 // minInterval. 64 expUpdater := mempool.ExponentialUpdater(0.001, maxInterval, minInterval) 65 chunks := 10 66 expectedAttempts := 10 67 68 withUpdaterScenario(t, chunks, expectedAttempts, expUpdater, func(t *testing.T, attempts uint64, lastTried time.Time, 69 retryAfter time.Duration) { 70 require.Equal(t, expectedAttempts, int(attempts)) // each chunk request should be attempted 10 times. 71 72 // request should NOT be immediately qualified for retrial due to exponential backoff. 73 require.True(t, !qualifier(attempts, lastTried, retryAfter)) 74 75 // expected retry after should be equal to the min interval, since updates should always underflow due 76 // to the very small multiplier. 77 require.Equal(t, minInterval, retryAfter) 78 }) 79 }) 80 81 t.Run("10 chunks- 10 times exponential updater- overflow", func(t *testing.T) { 82 // sets an exponential backoff updater with a maximum backoff of 1 hour, and minimum of a second. 83 minInterval := time.Second 84 maxInterval := time.Minute 85 // with exponential multiplier of 2, we expect to hit the overflow after 10 attempts. 86 expUpdater := mempool.ExponentialUpdater(2, maxInterval, minInterval) 87 chunks := 10 88 expectedAttempts := 10 89 90 withUpdaterScenario(t, chunks, expectedAttempts, expUpdater, func(t *testing.T, attempts uint64, lastTried time.Time, 91 retryAfter time.Duration) { 92 require.Equal(t, expectedAttempts, int(attempts)) // each chunk request should be attempted 10 times. 93 94 // request should NOT be immediately qualified for retrial due to exponential backoff. 95 require.True(t, !qualifier(attempts, lastTried, retryAfter)) 96 97 // expected retry after should be equal to the maxInterval, since updates should eventually overflow due 98 // to the very small maxInterval and quite noticeable multiplier (2). 99 require.Equal(t, maxInterval, retryAfter) 100 }) 101 }) 102 } 103 104 // withUpdaterScenario is a test helper that creates a chunk requests mempool and fills it with specified number of chunks. 105 // it then applies the updater on all of the chunks, and finally validates the chunks update history given the validator. 106 func withUpdaterScenario(t *testing.T, chunks int, times int, updater mempool.ChunkRequestHistoryUpdaterFunc, 107 validate func(*testing.T, uint64, time.Time, time.Duration)) { 108 109 // initializations: creating mempool and populating it. 110 requests := stdmap.NewChunkRequests(uint(chunks)) 111 chunkReqs := unittest.ChunkDataPackRequestListFixture(chunks) 112 for _, request := range chunkReqs { 113 ok := requests.Add(request) 114 require.True(t, ok) 115 } 116 117 // execution: updates request history of all chunks in mempool concurrently. 118 wg := &sync.WaitGroup{} 119 wg.Add(times * chunks) 120 for _, request := range chunkReqs { 121 for i := 0; i < times; i++ { 122 go func(chunkID flow.Identifier) { 123 _, _, _, ok := requests.UpdateRequestHistory(chunkID, updater) 124 require.True(t, ok) 125 126 wg.Done() 127 }(request.ChunkID) 128 } 129 } 130 unittest.RequireReturnsBefore(t, wg.Wait, 1*time.Second, "could not finish updating requests on time") 131 132 // performs custom validation of test. 133 for _, request := range chunkReqs { 134 attempts, lastTried, retryAfter, ok := requests.RequestHistory(request.ChunkID) 135 require.True(t, ok) 136 validate(t, attempts, lastTried, retryAfter) 137 } 138 } 139 140 // TestFailingUpdater evaluates the atomicity of updating request history. If an update is failing, none of the history 141 // attributes of a request should be altered. 142 func TestFailingUpdater(t *testing.T) { 143 // initializations: creating mempool and populating it, also updating each chunk request 144 // with an incremental updater. 145 requests := stdmap.NewChunkRequests(10) 146 chunkReqs := unittest.ChunkDataPackRequestListFixture(10) 147 for _, request := range chunkReqs { 148 ok := requests.Add(request) 149 require.True(t, ok) 150 } 151 152 wg := &sync.WaitGroup{} 153 wg.Add(10) 154 updater := mempool.IncrementalAttemptUpdater() 155 for _, request := range chunkReqs { 156 go func(chunkID flow.Identifier) { 157 attempts, _, _, ok := requests.UpdateRequestHistory(chunkID, updater) 158 require.True(t, ok) 159 require.Equal(t, uint64(1), attempts) 160 161 wg.Done() 162 }(request.ChunkID) 163 } 164 unittest.RequireReturnsBefore(t, wg.Wait, 1*time.Second, "could not finish updating requests on time") 165 166 // execution and validation: updating request history of all chunks in mempool concurrently using 167 // an updater that always failing should not change the requests' history 168 failingUpdater := func(uint64, time.Duration) (uint64, time.Duration, bool) { 169 return 0, 0, false 170 } 171 wg.Add(10) 172 for _, request := range chunkReqs { 173 go func(chunkID flow.Identifier) { 174 // takes request history before update 175 exAttempts, exLastTried, exRetryAfter, ok := requests.RequestHistory(chunkID) 176 require.True(t, ok) 177 178 // failing an update should not change request history 179 _, _, _, result := requests.UpdateRequestHistory(chunkID, failingUpdater) 180 require.False(t, result) 181 182 acAttempts, acLastTried, acRetryAfter, ok := requests.RequestHistory(chunkID) 183 require.True(t, ok) 184 require.Equal(t, exAttempts, acAttempts) 185 require.Equal(t, exLastTried, acLastTried) 186 require.Equal(t, exRetryAfter, acRetryAfter) 187 188 wg.Done() 189 }(request.ChunkID) 190 } 191 unittest.RequireReturnsBefore(t, wg.Wait, 1*time.Second, "could not finish updating requests on time") 192 } 193 194 // TestAddingDuplicateChunkIDs evaluates adding duplicate chunk ID requests 195 // that belong to an execution fork, i.e., same chunk ID appearing on two conflicting 196 // execution results. 197 func TestAddingDuplicateChunkIDs(t *testing.T) { 198 // initializations: creating mempool and populating it. 199 requests := stdmap.NewChunkRequests(10) 200 201 thisReq := unittest.ChunkDataPackRequestFixture() 202 require.True(t, requests.Add(thisReq)) 203 204 // adding another request for the same tuple of (chunkID, resultID, chunkIndex) 205 // is deduplicated. 206 require.False(t, requests.Add(&verification.ChunkDataPackRequest{ 207 Locator: chunks.Locator{ 208 ResultID: thisReq.ResultID, 209 Index: thisReq.Index, 210 }, 211 ChunkDataPackRequestInfo: verification.ChunkDataPackRequestInfo{ 212 ChunkID: thisReq.ChunkID, 213 }, 214 })) 215 216 // adding another request for the same chunk ID but different result ID is stored. 217 otherReq := &verification.ChunkDataPackRequest{ 218 Locator: chunks.Locator{ 219 ResultID: unittest.IdentifierFixture(), 220 Index: thisReq.Index, 221 }, 222 ChunkDataPackRequestInfo: verification.ChunkDataPackRequestInfo{ 223 ChunkID: thisReq.ChunkID, 224 Agrees: unittest.IdentifierListFixture(2), 225 Disagrees: unittest.IdentifierListFixture(2), 226 }, 227 } 228 require.True(t, requests.Add(otherReq)) 229 230 // mempool size is based on unique chunk ids, and we only store one 231 // chunk id. 232 require.Equal(t, requests.Size(), uint(1)) 233 234 // All method must return request info, which is also bound by chunk id. 235 reqInfoList := requests.All() 236 require.Len(t, reqInfoList, 1) 237 require.Equal(t, thisReq.ChunkID, reqInfoList[0].ChunkID) 238 // agrees, disagrees, and targets must be union of all requests for that chunk ID. 239 require.ElementsMatch(t, thisReq.Agrees.Union(otherReq.Agrees), reqInfoList[0].Agrees) 240 require.ElementsMatch(t, thisReq.Disagrees.Union(otherReq.Disagrees), reqInfoList[0].Disagrees) 241 242 var thisTargets flow.IdentifierList = thisReq.Targets.NodeIDs() 243 var otherTargets flow.IdentifierList = otherReq.Targets.NodeIDs() 244 require.ElementsMatch(t, thisTargets.Union(otherTargets), reqInfoList[0].Targets.NodeIDs()) 245 246 locators, ok := requests.PopAll(thisReq.ChunkID) 247 require.True(t, ok) 248 require.NotNil(t, locators[thisReq.Locator.ID()]) 249 require.NotNil(t, locators[otherReq.Locator.ID()]) 250 251 // after poping all, mempool must be empty (since the requests for the only 252 // chunk id have been poped). 253 require.Equal(t, requests.Size(), uint(0)) 254 255 // PopAll on a non-existing chunk ID must return false 256 locators, ok = requests.PopAll(thisReq.ChunkID) 257 require.False(t, ok) 258 require.Nil(t, locators) 259 }