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  }