github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/vent/chain/ethereum/consumer.go (about) 1 package ethereum 2 3 import ( 4 "bytes" 5 "fmt" 6 "time" 7 8 "github.com/hyperledger/burrow/logging" 9 "github.com/hyperledger/burrow/logging/structure" 10 "github.com/hyperledger/burrow/rpc/lib/types" 11 "github.com/pkg/errors" 12 13 "github.com/hyperledger/burrow/rpc/rpcevents" 14 "github.com/hyperledger/burrow/rpc/web3/ethclient" 15 "github.com/hyperledger/burrow/vent/chain" 16 ) 17 18 const ConsumerScope = "EthereumConsumer" 19 20 type consumer struct { 21 client ThrottleClient 22 filter *chain.Filter 23 blockRange *rpcevents.BlockRange 24 logger *logging.Logger 25 consumer func(block chain.Block) error 26 // Next unconsumed height 27 nextBlockHeight uint64 28 retries uint64 29 baseBackoffDuration time.Duration 30 backoffDuration time.Duration 31 maxRetries uint64 32 maxBlockBatchSize uint64 33 blockBatchSize uint64 34 } 35 36 func Consume(client ThrottleClient, filter *chain.Filter, blockRange *rpcevents.BlockRange, config *chain.BlockConsumerConfig, 37 logger *logging.Logger, consume func(block chain.Block) error) error { 38 c := consumer{ 39 client: client, 40 filter: filter, 41 blockRange: blockRange, 42 logger: logger.WithScope(ConsumerScope), 43 consumer: consume, 44 baseBackoffDuration: config.BaseBackoffDuration, 45 backoffDuration: config.BaseBackoffDuration, 46 maxRetries: config.MaxRetries, 47 maxBlockBatchSize: config.MaxBlockBatchSize, 48 blockBatchSize: config.MaxBlockBatchSize, 49 } 50 return c.Consume() 51 } 52 53 func (c *consumer) Consume() error { 54 start, end, streaming, err := c.bounds() 55 if err != nil { 56 return err 57 } 58 c.logger.TraceMsg("Consume", "start", start, "end", end, "streaming", streaming) 59 60 for c.nextBlockHeight <= end || streaming { 61 err = c.ConsumeInBatches(start, end) 62 if err != nil { 63 return err 64 } 65 start, end, streaming, err = c.bounds() 66 if err != nil { 67 return err 68 } 69 // Avoid spinning excessively where there may be no blocks available 70 time.Sleep(c.backoffDuration) 71 } 72 73 return nil 74 } 75 76 func (c *consumer) ConsumeInBatches(start, end uint64) error { 77 c.logger.TraceMsg("ConsumeInBatches", "start", start, "end", end) 78 for batchStart := start; batchStart <= end; batchStart += c.blockBatchSize { 79 // Avoid breaching requests limit 80 c.client.Throttle() 81 batchEnd := batchStart + c.blockBatchSize 82 c.logger.TraceMsg("Consuming batch", "batch_start", batchStart, "batch_end", batchEnd) 83 if batchEnd > end { 84 batchEnd = end 85 } 86 logs, err := c.client.GetLogs(ðclient.Filter{ 87 BlockRange: rpcevents.AbsoluteRange(batchStart, batchEnd), 88 Addresses: c.filter.Addresses, 89 Topics: c.filter.Topics, 90 }) 91 if err != nil { 92 err = c.handleError(end, err) 93 if err != nil { 94 return err 95 } 96 // We managed to handle the error (a retry was successful) 97 return nil 98 } 99 // Request was successful 100 c.recover() 101 lastBlock, err := consumeBlocksFromLogs(c.client, logs, c.consumer) 102 if err != nil { 103 return fmt.Errorf("could not consume ethereum logs: %w", err) 104 } 105 if lastBlock != nil { 106 c.nextBlockHeight = lastBlock.GetHeight() + 1 107 } 108 c.logger.TraceMsg("Finished consuming batch", "next_block_height", c.nextBlockHeight) 109 } 110 return nil 111 } 112 113 func (c *consumer) bounds() (start uint64, end uint64, streaming bool, err error) { 114 var latestHeight uint64 115 116 latestHeight, err = c.client.BlockNumber() 117 if err != nil { 118 err = fmt.Errorf("could not get latest height: %w", err) 119 return 120 } 121 start, end, streaming = c.blockRange.Bounds(latestHeight) 122 123 if start < c.nextBlockHeight { 124 start = c.nextBlockHeight 125 } 126 return 127 } 128 129 func (c *consumer) handleError(end uint64, err error) error { 130 var rpcError *types.RPCError 131 if errors.As(err, &rpcError) { 132 // If we have a custom server error maybe our batch size is too large or maybe we should wait 133 if rpcError.IsServerError() { 134 c.retries++ 135 c.logger.InfoMsg("caught Ethereum server error, backing off...", 136 structure.ErrorKey, err, "retry", c.retries, "backoff", c.backoffDuration.String()) 137 if c.retries <= c.maxRetries { 138 // Server may throw if batch too large or request takes too long 139 c.backoff() 140 c.logger.InfoMsg("Ethereum block consumer retrying after Ethereum Server Error", 141 structure.ErrorKey, rpcError) 142 return c.ConsumeInBatches(c.nextBlockHeight, end) 143 } 144 } 145 } 146 return err 147 } 148 149 // Asymptotic decrease to single block 150 func (c *consumer) backoff() { 151 c.blockBatchSize /= 2 152 if c.blockBatchSize == 0 { 153 c.blockBatchSize = 1 154 } 155 time.Sleep(c.backoffDuration) 156 c.backoffDuration *= 2 157 } 158 159 // Asymptotic increase to max blocks 160 func (c *consumer) recover() { 161 delta := (c.maxBlockBatchSize - c.blockBatchSize) / 2 162 if delta == 0 { 163 c.blockBatchSize = c.maxBlockBatchSize 164 } else { 165 c.blockBatchSize += delta 166 } 167 // Reset retries and backoff 168 c.backoffDuration = c.baseBackoffDuration 169 c.retries = 0 170 } 171 172 func consumeBlocksFromLogs(client EthClient, logs []*ethclient.EthLog, 173 consumer func(block chain.Block) error) (chain.Block, error) { 174 if len(logs) == 0 { 175 return nil, nil 176 } 177 log, err := newEvent(logs[0]) 178 if err != nil { 179 return nil, fmt.Errorf("could not deserialise ethereum event: %w", err) 180 } 181 block := newBlock(client, log) 182 txHash := log.TransactionHash 183 indexInBlock := log.IndexInBlock 184 185 for i := 1; i < len(logs); i++ { 186 log, err = newEvent(logs[i]) 187 if err != nil { 188 return nil, fmt.Errorf("could not deserialise ethereum event: %w", err) 189 } 190 if log.Height > block.Height { 191 // New block 192 err = consumer(block) 193 if err != nil { 194 return nil, err 195 } 196 // Establish new block 197 block = newBlock(client, log) 198 } else { 199 if log.IndexInBlock <= indexInBlock { 200 return nil, fmt.Errorf("event LogIndex is non-increasing within block, "+ 201 "previous LogIndex was %d but current is %d (at height %d)", indexInBlock, log.IndexInBlock, block.Height) 202 } 203 if !bytes.Equal(txHash, log.TransactionHash) { 204 // New Tx 205 block.appendTransaction(log) 206 } else { 207 block.appendEvent(log) 208 } 209 } 210 txHash = log.TransactionHash 211 indexInBlock = log.IndexInBlock 212 } 213 return block, consumer(block) 214 }