github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/backend/retry_test.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/onflow/flow/protobuf/go/flow/access"
     7  	"github.com/onflow/flow/protobuf/go/flow/entities"
     8  	"github.com/onflow/flow/protobuf/go/flow/execution"
     9  	"github.com/stretchr/testify/mock"
    10  	"google.golang.org/grpc/codes"
    11  	"google.golang.org/grpc/status"
    12  
    13  	"github.com/onflow/flow-go/model/flow"
    14  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    15  	realstorage "github.com/onflow/flow-go/storage"
    16  	"github.com/onflow/flow-go/utils/unittest"
    17  )
    18  
    19  // TestTransactionRetry tests that the retry mechanism will send retries at specific times
    20  func (suite *Suite) TestTransactionRetry() {
    21  	collection := unittest.CollectionFixture(1)
    22  	transactionBody := collection.Transactions[0]
    23  	block := unittest.BlockFixture()
    24  	// Height needs to be at least DefaultTransactionExpiry before we start doing retries
    25  	block.Header.Height = flow.DefaultTransactionExpiry + 1
    26  	transactionBody.SetReferenceBlockID(block.ID())
    27  	headBlock := unittest.BlockFixture()
    28  	headBlock.Header.Height = block.Header.Height - 1 // head is behind the current block
    29  	suite.state.On("Final").Return(suite.snapshot, nil).Maybe()
    30  
    31  	suite.snapshot.On("Head").Return(headBlock.Header, nil)
    32  	snapshotAtBlock := new(protocol.Snapshot)
    33  	snapshotAtBlock.On("Head").Return(block.Header, nil)
    34  	suite.state.On("AtBlockID", block.ID()).Return(snapshotAtBlock, nil)
    35  
    36  	// collection storage returns a not found error
    37  	suite.collections.On("LightByTransactionID", transactionBody.ID()).Return(nil, realstorage.ErrNotFound)
    38  
    39  	params := suite.defaultBackendParams()
    40  
    41  	// Setup Handler + Retry
    42  	backend, err := New(params)
    43  	suite.Require().NoError(err)
    44  
    45  	retry := newRetry(suite.log).SetBackend(backend).Activate()
    46  	backend.retry = retry
    47  
    48  	retry.RegisterTransaction(block.Header.Height, transactionBody)
    49  
    50  	suite.colClient.On("SendTransaction", mock.Anything, mock.Anything).Return(&access.SendTransactionResponse{}, nil)
    51  
    52  	// Don't retry on every height
    53  	err = retry.Retry(block.Header.Height + 1)
    54  	suite.Require().NoError(err)
    55  
    56  	suite.colClient.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything)
    57  
    58  	// Retry every `retryFrequency`
    59  	err = retry.Retry(block.Header.Height + retryFrequency)
    60  	suite.Require().NoError(err)
    61  
    62  	suite.colClient.AssertNumberOfCalls(suite.T(), "SendTransaction", 1)
    63  
    64  	// do not retry if expired
    65  	err = retry.Retry(block.Header.Height + retryFrequency + flow.DefaultTransactionExpiry)
    66  	suite.Require().NoError(err)
    67  
    68  	// Should've still only been called once
    69  	suite.colClient.AssertNumberOfCalls(suite.T(), "SendTransaction", 1)
    70  
    71  	suite.assertAllExpectations()
    72  }
    73  
    74  // TestSuccessfulTransactionsDontRetry tests that the retry mechanism will send retries at specific times
    75  func (suite *Suite) TestSuccessfulTransactionsDontRetry() {
    76  	ctx := context.Background()
    77  	collection := unittest.CollectionFixture(1)
    78  	transactionBody := collection.Transactions[0]
    79  	block := unittest.BlockFixture()
    80  	// Height needs to be at least DefaultTransactionExpiry before we start doing retries
    81  	block.Header.Height = flow.DefaultTransactionExpiry + 1
    82  	refBlock := unittest.BlockFixture()
    83  	refBlock.Header.Height = 2
    84  	transactionBody.SetReferenceBlockID(refBlock.ID())
    85  
    86  	block.SetPayload(
    87  		unittest.PayloadFixture(
    88  			unittest.WithGuarantees(
    89  				unittest.CollectionGuaranteesWithCollectionIDFixture([]*flow.Collection{&collection})...)))
    90  
    91  	light := collection.Light()
    92  	suite.state.On("Final").Return(suite.snapshot, nil).Maybe()
    93  	// transaction storage returns the corresponding transaction
    94  	suite.transactions.On("ByID", transactionBody.ID()).Return(transactionBody, nil)
    95  	// collection storage returns the corresponding collection
    96  	suite.collections.On("LightByTransactionID", transactionBody.ID()).Return(&light, nil)
    97  	suite.collections.On("LightByID", light.ID()).Return(&light, nil)
    98  	// block storage returns the corresponding block
    99  	suite.blocks.On("ByCollectionID", collection.ID()).Return(&block, nil)
   100  
   101  	txID := transactionBody.ID()
   102  	blockID := block.ID()
   103  	exeEventReq := execution.GetTransactionResultRequest{
   104  		BlockId:       blockID[:],
   105  		TransactionId: txID[:],
   106  	}
   107  	exeEventResp := execution.GetTransactionResultResponse{
   108  		Events: nil,
   109  	}
   110  
   111  	_, enIDs := suite.setupReceipts(&block)
   112  	suite.snapshot.On("Identities", mock.Anything).Return(enIDs, nil)
   113  	connFactory := suite.setupConnectionFactory()
   114  
   115  	params := suite.defaultBackendParams()
   116  	params.ConnFactory = connFactory
   117  
   118  	backend, err := New(params)
   119  	suite.Require().NoError(err)
   120  
   121  	retry := newRetry(suite.log).SetBackend(backend).Activate()
   122  	backend.retry = retry
   123  
   124  	retry.RegisterTransaction(block.Header.Height, transactionBody)
   125  
   126  	suite.colClient.On("SendTransaction", mock.Anything, mock.Anything).Return(&access.SendTransactionResponse{}, nil)
   127  
   128  	// return not found to return finalized status
   129  	suite.execClient.On("GetTransactionResult", ctx, &exeEventReq).
   130  		Return(&exeEventResp, status.Errorf(codes.NotFound, "not found")).
   131  		Times(len(enIDs)) // should call each EN once
   132  
   133  	// first call - when block under test is greater height than the sealed head, but execution node does not know about Tx
   134  	result, err := backend.GetTransactionResult(
   135  		ctx,
   136  		txID,
   137  		flow.ZeroID,
   138  		flow.ZeroID,
   139  		entities.EventEncodingVersion_JSON_CDC_V0,
   140  	)
   141  	suite.checkResponse(result, err)
   142  
   143  	// status should be finalized since the sealed Blocks is smaller in height
   144  	suite.Assert().Equal(flow.TransactionStatusFinalized, result.Status)
   145  
   146  	// Don't retry now now that block is finalized
   147  	err = retry.Retry(block.Header.Height + 1)
   148  	suite.Require().NoError(err)
   149  
   150  	suite.colClient.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything)
   151  
   152  	// Don't retry now now that block is finalized
   153  	err = retry.Retry(block.Header.Height + retryFrequency)
   154  	suite.Require().NoError(err)
   155  
   156  	suite.colClient.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything)
   157  
   158  	// Don't retry now now that block is finalized
   159  	err = retry.Retry(block.Header.Height + retryFrequency + flow.DefaultTransactionExpiry)
   160  	suite.Require().NoError(err)
   161  
   162  	// Should've still should not be called
   163  	suite.colClient.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything)
   164  
   165  	suite.assertAllExpectations()
   166  }