mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
Co-authored-by: dhrubabasu <[email protected]> Co-authored-by: Patrick O'Grady <[email protected]> Co-authored-by: Stephen <[email protected]>
609 lines
14 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|