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 }