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(&ethclient.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  }