Files
indexer/stream/producer_cchain.go
3a9331169b Update avalanchego to v1.9.0-fuji (#472)
Co-authored-by: dhrubabasu <[email protected]>
Co-authored-by: Patrick O'Grady <[email protected]>
Co-authored-by: Stephen <[email protected]>
2022-10-02 20:20:49 -04:00

609 lines
14 KiB
Go

// (c) 2021, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package stream
import (
"context"
"encoding/json"
"fmt"
"io"
"math/big"
"strings"
"sync"
"time"
"github.com/ava-labs/avalanchego/ids"
avalancheGoUtils "github.com/ava-labs/avalanchego/utils"
"github.com/ava-labs/avalanchego/utils/hashing"
"github.com/ava-labs/avalanchego/utils/wrappers"
"github.com/ava-labs/ortelius/cfg"
"github.com/ava-labs/ortelius/db"
"github.com/ava-labs/ortelius/modelsc"
"github.com/ava-labs/ortelius/servicesctrl"
"github.com/ava-labs/ortelius/utils"
"go.uber.org/zap"
)
const (
rpcTimeout = time.Minute
dbReadTimeout = 10 * time.Second
dbWriteTimeout = time.Minute
readRPCTimeout = 500 * time.Millisecond
maxWorkerQueue = 4000
maxWorkers = 8
)
type producerCChainContainer struct {
sc *servicesctrl.Control
conns *utils.Connections
block *big.Int
blockCount *big.Int
client *modelsc.Client
msgChan chan *blockWorkContainer
msgChanDone chan struct{}
runningControl utils.Running
catchupErrs avalancheGoUtils.AtomicInterface
}
func newContainerC(
sc *servicesctrl.Control,
conf cfg.Config,
) (*producerCChainContainer, error) {
conns, err := sc.Database()
if err != nil {
return nil, err
}
pc := &producerCChainContainer{
msgChan: make(chan *blockWorkContainer, maxWorkerQueue),
msgChanDone: make(chan struct{}, 1),
runningControl: utils.NewRunning(),
conns: conns,
sc: sc,
}
err = pc.getBlock()
if err != nil {
_ = conns.Close()
return nil, err
}
cl, err := modelsc.NewClient(conf.AvalancheGO + "/ext/bc/C/rpc")
if err != nil {
_ = conns.Close()
return nil, err
}
pc.client = cl
return pc, nil
}
func (p *producerCChainContainer) Close() error {
if p.client != nil {
p.client.Close()
}
errs := wrappers.Errs{}
if p.conns != nil {
errs.Add(p.conns.Close())
}
return errs.Err
}
func (p *producerCChainContainer) getBlock() error {
var err error
sess := p.conns.DB().NewSessionForEventReceiver(p.conns.Stream().NewJob("get-block"))
ctx, cancelCtx := context.WithTimeout(context.Background(), dbReadTimeout)
defer cancelCtx()
type MaxBlock struct {
Block string
BlockCount string
}
maxBlock := MaxBlock{}
_, err = sess.Select(
"cast(case when max(block) is null then -1 else max(block) end as char) as block",
"cast(count(*) as char) as block_count",
).
From(db.TableCvmBlocks).
LoadContext(ctx, &maxBlock)
if err != nil {
return err
}
mblock, ok := big.NewInt(0).SetString(maxBlock.Block, 10)
if !ok {
return fmt.Errorf("invalid block %s", maxBlock.Block)
}
cblockCount, ok := big.NewInt(0).SetString(maxBlock.BlockCount, 10)
if !ok {
return fmt.Errorf("invalid block %s", maxBlock.BlockCount)
}
p.block = mblock
p.blockCount = cblockCount
p.sc.Log.Info("stating processing block",
zap.Stringer("block", p.block),
zap.Stringer("count", p.blockCount),
)
return nil
}
func (p *producerCChainContainer) catchupBlock(conns *utils.Connections, catchupBlock *big.Int, wg *sync.WaitGroup) {
defer func() {
wg.Done()
_ = conns.Close()
}()
sess := conns.DB().NewSessionForEventReceiver(conns.Stream().NewJob("catchup-block"))
ctx := context.Background()
var err error
startBlock := big.NewInt(0)
endBlock := big.NewInt(0)
for endBlock.Cmp(catchupBlock) < 0 {
if p.runningControl.IsStopped() {
break
}
if p.catchupErrs.GetValue() != nil {
return
}
endBlock = big.NewInt(0).Add(startBlock, big.NewInt(100000))
if endBlock.Cmp(catchupBlock) >= 0 {
endBlock.Set(catchupBlock)
}
var cvmBlocks []*db.CvmBlocks
_, err = sess.Select(
"block",
).From(db.TableCvmBlocks).
Where("block >= "+startBlock.String()+" and block < "+endBlock.String()).
LoadContext(ctx, &cvmBlocks)
if err != nil {
p.catchupErrs.SetValue(err)
return
}
blockMap := make(map[string]struct{})
for _, bl := range cvmBlocks {
blockMap[bl.Block] = struct{}{}
}
for startBlock.Cmp(endBlock) < 0 {
if p.runningControl.IsStopped() {
break
}
if p.catchupErrs.GetValue() != nil {
return
}
startBlockStr := startBlock.String()
if _, ok := blockMap[startBlockStr]; !ok {
p.sc.Log.Info("refilling message chan",
zap.String("block", startBlockStr),
)
p.msgChan <- &blockWorkContainer{errs: &p.catchupErrs, blockNumber: startBlock}
}
startBlock = big.NewInt(0).Add(startBlock, big.NewInt(1))
}
}
p.sc.Log.Info("catchup complete")
}
func (p *producerCChainContainer) ProcessNextMessage() error {
lblocknext, err := p.client.Latest(rpcTimeout)
if err != nil {
time.Sleep(readRPCTimeout)
return err
}
if lblocknext.Cmp(p.block) <= 0 {
time.Sleep(readRPCTimeout)
return ErrNoMessage
}
errs := &avalancheGoUtils.AtomicInterface{}
for lblocknext.Cmp(p.block) > 0 {
if p.runningControl.IsStopped() {
break
}
if errs.GetValue() != nil {
return errs.GetValue().(error)
}
if p.catchupErrs.GetValue() != nil {
return p.catchupErrs.GetValue().(error)
}
ncurrent := big.NewInt(0).Add(p.block, big.NewInt(1))
p.msgChan <- &blockWorkContainer{errs: errs, blockNumber: ncurrent}
p.block = big.NewInt(0).Add(p.block, big.NewInt(1))
}
for len(p.msgChan) > 0 {
time.Sleep(1 * time.Millisecond)
}
if errs.GetValue() != nil {
return errs.GetValue().(error)
}
return nil
}
type ProducerCChain struct {
id string
sc *servicesctrl.Control
// metrics
metricProcessedCountKey string
metricSuccessCountKey string
metricFailureCountKey string
conf cfg.Config
runningControl utils.Running
topic string
topicTrc string
topicLogs string
}
func NewProducerCChain(sc *servicesctrl.Control, conf cfg.Config) utils.ListenCloser {
topicName := fmt.Sprintf("%d-%s-cchain", conf.NetworkID, conf.CchainID)
topicTrcName := fmt.Sprintf("%d-%s-cchain-trc", conf.NetworkID, conf.CchainID)
topicLogsName := fmt.Sprintf("%d-%s-cchain-logs", conf.NetworkID, conf.CchainID)
p := &ProducerCChain{
topic: topicName,
topicTrc: topicTrcName,
topicLogs: topicLogsName,
conf: conf,
sc: sc,
metricProcessedCountKey: fmt.Sprintf("produce_records_processed_%s_cchain", conf.CchainID),
metricSuccessCountKey: fmt.Sprintf("produce_records_success_%s_cchain", conf.CchainID),
metricFailureCountKey: fmt.Sprintf("produce_records_failure_%s_cchain", conf.CchainID),
id: fmt.Sprintf("producer %d %s cchain", conf.NetworkID, conf.CchainID),
runningControl: utils.NewRunning(),
}
utils.Prometheus.CounterInit(p.metricProcessedCountKey, "records processed")
utils.Prometheus.CounterInit(p.metricSuccessCountKey, "records success")
utils.Prometheus.CounterInit(p.metricFailureCountKey, "records failure")
sc.InitProduceMetrics()
return p
}
func (p *ProducerCChain) Close() error {
p.runningControl.Close()
return nil
}
func (p *ProducerCChain) ID() string {
return p.id
}
func (p *ProducerCChain) updateBlock(conns *utils.Connections, blockNumber *big.Int, updateTime time.Time) error {
sess := conns.DB().NewSessionForEventReceiver(conns.Stream().NewJob("update-block"))
ctx, cancelCtx := context.WithTimeout(context.Background(), dbWriteTimeout)
defer cancelCtx()
cvmBlocks := &db.CvmBlocks{
Block: blockNumber.String(),
CreatedAt: updateTime,
}
return p.sc.Persist.InsertCvmBlocks(ctx, sess, cvmBlocks)
}
func (p *ProducerCChain) Failure() {
_ = utils.Prometheus.CounterInc(p.metricFailureCountKey)
_ = utils.Prometheus.CounterInc(servicesctrl.MetricProduceFailureCountKey)
}
func (p *ProducerCChain) Success() {
_ = utils.Prometheus.CounterInc(p.metricSuccessCountKey)
_ = utils.Prometheus.CounterInc(servicesctrl.MetricProduceSuccessCountKey)
}
func (p *ProducerCChain) Listen() error {
p.sc.Log.Info("Started worker manager for cchain")
defer p.sc.Log.Info("Exiting worker manager for cchain")
for !p.runningControl.IsStopped() {
err := p.runProcessor()
// If there was an error we want to log it, and iff we are not stopping
// we want to add a retry delay.
if err != nil {
p.sc.Log.Error("error running worker",
zap.Error(err),
)
}
if p.runningControl.IsStopped() {
break
}
if err != nil {
<-time.After(processorFailureRetryInterval)
}
}
return nil
}
func CChainNotReady(err error) bool {
if strings.HasPrefix(err.Error(), "404 Not Found") {
return true
}
if strings.HasPrefix(err.Error(), "503 Service Unavailable") {
return true
}
if strings.HasSuffix(err.Error(), "connect: connection refused") {
return true
}
if strings.HasSuffix(err.Error(), "read: connection reset by peer") {
return true
}
return false
}
// runProcessor starts the processing loop for the backend and closes it when
// finished
func (p *ProducerCChain) runProcessor() error {
if p.runningControl.IsStopped() {
p.sc.Log.Info("Not starting worker for cchain because we're stopping")
return nil
}
p.sc.Log.Info("Starting worker for cchain")
defer p.sc.Log.Info("Exiting worker for cchain")
wgpc := &sync.WaitGroup{}
wgpcmsgchan := &sync.WaitGroup{}
pc, err := newContainerC(p.sc, p.conf)
if err != nil {
return err
}
defer func() {
pc.runningControl.Close()
wgpc.Wait()
close(pc.msgChanDone)
wgpcmsgchan.Wait()
close(pc.msgChan)
err := pc.Close()
if err != nil {
p.sc.Log.Warn("stopping worker for cchain",
zap.Error(err),
)
}
}()
pblockp1 := big.NewInt(0).Add(pc.block, big.NewInt(1))
if pc.blockCount.Cmp(pblockp1) < 0 {
conns1, err := p.sc.Database()
if err != nil {
return err
}
wgpc.Add(1)
go pc.catchupBlock(conns1, pblockp1, wgpc)
}
for icnt := 0; icnt < maxWorkers; icnt++ {
cl, err := modelsc.NewClient(p.conf.AvalancheGO + "/ext/bc/C/rpc")
if err != nil {
return err
}
conns1, err := p.sc.Database()
if err != nil {
cl.Close()
return err
}
wgpcmsgchan.Add(1)
go p.blockProcessor(pc, cl, conns1, wgpcmsgchan)
}
// Create a closure that processes the next message from the backend
processNextMessage := func() error {
err := pc.ProcessNextMessage()
if errIntf := pc.catchupErrs.GetValue(); errIntf != nil {
err := errIntf.(error)
if !CChainNotReady(err) {
p.Failure()
p.sc.Log.Error("catchup error",
zap.Error(err),
)
} else {
p.sc.Log.Warn("catchup error",
zap.Error(err),
)
}
return err
}
switch err {
case nil:
p.Success()
return nil
// This error is expected when the upstream service isn't producing
case context.DeadlineExceeded:
p.sc.Log.Debug("context deadline exceeded")
return nil
case ErrNoMessage:
return nil
case io.EOF:
p.sc.Log.Error("EOF")
return io.EOF
default:
if CChainNotReady(err) {
p.sc.Log.Warn("chain not ready when processing message",
zap.Error(err),
)
return nil
}
p.Failure()
p.sc.Log.Error("unknown error when processing message",
zap.Error(err),
)
return err
}
}
// Process messages until asked to stop
for {
if p.runningControl.IsStopped() || pc.runningControl.IsStopped() {
break
}
err := processNextMessage()
if err != nil {
return err
}
}
return nil
}
type localBlockObject struct {
blockContainer *modelsc.BlockContainer
time time.Time
}
func (p *ProducerCChain) processWork(conns *utils.Connections, localBlock *localBlockObject) error {
cblk, err := modelsc.New(localBlock.blockContainer.Block)
if err != nil {
return err
}
for _, txTranactionTraces := range localBlock.blockContainer.Traces {
txTransactionTracesBits, err := json.Marshal(txTranactionTraces)
if err != nil {
return err
}
idsv := fmt.Sprintf("%s:%d", txTranactionTraces.Hash, txTranactionTraces.Idx)
id := ids.ID(hashing.ComputeHash256Array([]byte(idsv)))
txPool := &db.TxPool{
NetworkID: p.conf.NetworkID,
ChainID: p.conf.CchainID,
MsgKey: id.String(),
Serialization: txTransactionTracesBits,
Processed: 0,
Topic: p.topicTrc,
CreatedAt: localBlock.time,
}
txPool.ComputeID()
err = UpdateTxPool(dbWriteTimeout, conns, p.sc.Persist, txPool, p.sc)
if err != nil {
return err
}
}
for _, log := range localBlock.blockContainer.Logs {
logBits, err := json.Marshal(log)
if err != nil {
return err
}
idsv := fmt.Sprintf("%s:%s:%d", log.BlockHash, log.TxHash, log.Index)
id := ids.ID(hashing.ComputeHash256Array([]byte(idsv)))
txPool := &db.TxPool{
NetworkID: p.conf.NetworkID,
ChainID: p.conf.CchainID,
MsgKey: id.String(),
Serialization: logBits,
Processed: 0,
Topic: p.topicLogs,
CreatedAt: localBlock.time,
}
txPool.ComputeID()
err = UpdateTxPool(dbWriteTimeout, conns, p.sc.Persist, txPool, p.sc)
if err != nil {
return err
}
}
block, err := json.Marshal(cblk)
if err != nil {
return err
}
id := ids.ID(hashing.ComputeHash256Array([]byte(cblk.Header.Number.String())))
txPool := &db.TxPool{
NetworkID: p.conf.NetworkID,
ChainID: p.conf.CchainID,
MsgKey: id.String(),
Serialization: block,
Processed: 0,
Topic: p.topic,
CreatedAt: localBlock.time,
}
txPool.ComputeID()
err = UpdateTxPool(dbWriteTimeout, conns, p.sc.Persist, txPool, p.sc)
if err != nil {
return err
}
return nil
}
type blockWorkContainer struct {
errs *avalancheGoUtils.AtomicInterface
blockNumber *big.Int
}
func (p *ProducerCChain) blockProcessor(pc *producerCChainContainer, client *modelsc.Client, conns *utils.Connections, wg *sync.WaitGroup) {
defer func() {
wg.Done()
_ = conns.Close()
client.Close()
}()
for {
select {
case <-pc.msgChanDone:
return
case blockWork := <-pc.msgChan:
if blockWork.errs.GetValue() != nil {
continue
}
blContainer, err := client.ReadBlock(blockWork.blockNumber, rpcTimeout)
if err != nil {
blockWork.errs.SetValue(err)
continue
}
localBlockObject := &localBlockObject{blockContainer: blContainer, time: time.Now()}
err = p.processWork(conns, localBlockObject)
if err != nil {
blockWork.errs.SetValue(err)
continue
}
err = p.updateBlock(conns, blockWork.blockNumber, localBlockObject.time)
if err != nil {
blockWork.errs.SetValue(err)
continue
}
_ = utils.Prometheus.CounterInc(p.metricProcessedCountKey)
_ = utils.Prometheus.CounterInc(servicesctrl.MetricProduceProcessedCountKey)
}
}
}