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

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  
     9  	"github.com/rs/zerolog"
    10  
    11  	"github.com/onflow/flow-go/model/flow"
    12  	"github.com/onflow/flow-go/state"
    13  	"github.com/onflow/flow-go/storage"
    14  )
    15  
    16  // retryFrequency has to be less than TransactionExpiry or else this module does nothing
    17  const retryFrequency uint64 = 120 // Blocks
    18  
    19  // Retry implements a simple retry mechanism for transaction submission.
    20  type Retry struct {
    21  	mu sync.RWMutex
    22  	// pending Transactions
    23  	transactionByReferencBlockHeight map[uint64]map[flow.Identifier]*flow.TransactionBody
    24  	backend                          *Backend
    25  	active                           bool
    26  	log                              zerolog.Logger // default logger
    27  }
    28  
    29  func newRetry(log zerolog.Logger) *Retry {
    30  	return &Retry{
    31  		log:                              log,
    32  		transactionByReferencBlockHeight: map[uint64]map[flow.Identifier]*flow.TransactionBody{},
    33  	}
    34  }
    35  
    36  func (r *Retry) Activate() *Retry {
    37  	r.active = true
    38  	return r
    39  }
    40  
    41  func (r *Retry) IsActive() bool {
    42  	return r.active
    43  }
    44  
    45  func (r *Retry) SetBackend(b *Backend) *Retry {
    46  	r.backend = b
    47  	return r
    48  }
    49  
    50  // Retry attempts to resend transactions for a specified block height.
    51  // It performs cleanup operations, including pruning old transactions, and retries sending
    52  // transactions that are still pending.
    53  // The method takes a block height as input. If the provided height is lower than
    54  // flow.DefaultTransactionExpiry, no retries are performed, and the method returns nil.
    55  // No errors expected during normal operations.
    56  func (r *Retry) Retry(height uint64) error {
    57  	// No need to retry if height is lower than DefaultTransactionExpiry
    58  	if height < flow.DefaultTransactionExpiry {
    59  		return nil
    60  	}
    61  
    62  	// naive cleanup for now, prune every 120 Blocks
    63  	if height%retryFrequency == 0 {
    64  		r.prune(height)
    65  	}
    66  
    67  	heightToRetry := height - flow.DefaultTransactionExpiry + retryFrequency
    68  
    69  	for heightToRetry < height {
    70  		err := r.retryTxsAtHeight(heightToRetry)
    71  		if err != nil {
    72  			return err
    73  		}
    74  		heightToRetry = heightToRetry + retryFrequency
    75  	}
    76  	return nil
    77  }
    78  
    79  // RegisterTransaction adds a transaction that could possibly be retried
    80  func (r *Retry) RegisterTransaction(height uint64, tx *flow.TransactionBody) {
    81  	r.mu.Lock()
    82  	defer r.mu.Unlock()
    83  	if r.transactionByReferencBlockHeight[height] == nil {
    84  		r.transactionByReferencBlockHeight[height] = make(map[flow.Identifier]*flow.TransactionBody)
    85  	}
    86  	r.transactionByReferencBlockHeight[height][tx.ID()] = tx
    87  }
    88  
    89  func (r *Retry) prune(height uint64) {
    90  	r.mu.Lock()
    91  	defer r.mu.Unlock()
    92  	// If height is less than the default, there will be no expired Transactions
    93  	if height < flow.DefaultTransactionExpiry {
    94  		return
    95  	}
    96  	for h := range r.transactionByReferencBlockHeight {
    97  		if h < height-flow.DefaultTransactionExpiry {
    98  			delete(r.transactionByReferencBlockHeight, h)
    99  		}
   100  	}
   101  }
   102  
   103  // retryTxsAtHeight retries transactions at a specific block height.
   104  // It looks up transactions at the specified height and retries sending
   105  // raw transactions for those that are still pending. It also cleans up
   106  // transactions that are no longer pending or have an unknown status.
   107  // Error returns:
   108  //   - errors are unexpected and potentially symptoms of internal implementation bugs or state corruption (fatal).
   109  func (r *Retry) retryTxsAtHeight(heightToRetry uint64) error {
   110  	r.mu.Lock()
   111  	defer r.mu.Unlock()
   112  	txsAtHeight := r.transactionByReferencBlockHeight[heightToRetry]
   113  	for txID, tx := range txsAtHeight {
   114  		// find the block for the transaction
   115  		block, err := r.backend.lookupBlock(txID)
   116  		if err != nil {
   117  			if !errors.Is(err, storage.ErrNotFound) {
   118  				return err
   119  			}
   120  			block = nil
   121  		}
   122  
   123  		// find the transaction status
   124  		var status flow.TransactionStatus
   125  		if block == nil {
   126  			status, err = r.backend.DeriveUnknownTransactionStatus(tx.ReferenceBlockID)
   127  		} else {
   128  			status, err = r.backend.DeriveTransactionStatus(block.Header.Height, false)
   129  		}
   130  
   131  		if err != nil {
   132  			if !errors.Is(err, state.ErrUnknownSnapshotReference) {
   133  				return err
   134  			}
   135  			continue
   136  		}
   137  		if status == flow.TransactionStatusPending {
   138  			err = r.backend.SendRawTransaction(context.Background(), tx)
   139  			if err != nil {
   140  				r.log.Info().Str("retry", fmt.Sprintf("retryTxsAtHeight: %v", heightToRetry)).Err(err).Msg("failed to send raw transactions")
   141  			}
   142  		} else if status != flow.TransactionStatusUnknown {
   143  			// not pending or unknown, don't need to retry anymore
   144  			delete(txsAtHeight, txID)
   145  		}
   146  	}
   147  	return nil
   148  }