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]>
714 lines
15 KiB
Go
714 lines
15 KiB
Go
// (c) 2021, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package balance
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/ava-labs/ortelius/db"
|
|
"github.com/ava-labs/ortelius/servicesctrl"
|
|
"github.com/ava-labs/ortelius/utils"
|
|
"github.com/gocraft/dbr/v2"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var RowLimitValue = uint64(5000)
|
|
var LockSize = 5
|
|
|
|
var retryProcessing = 3
|
|
|
|
var updTimeout = 1 * time.Minute
|
|
|
|
type processType uint32
|
|
|
|
var processTypeIn processType = 1
|
|
var processTypeOut processType = 2
|
|
|
|
type managerChannels struct {
|
|
ins chan struct{}
|
|
outs chan struct{}
|
|
txs chan struct{}
|
|
}
|
|
|
|
func newManagerChannels() *managerChannels {
|
|
return &managerChannels{
|
|
ins: make(chan struct{}, 1),
|
|
outs: make(chan struct{}, 1),
|
|
txs: make(chan struct{}, 1),
|
|
}
|
|
}
|
|
|
|
type Manager struct {
|
|
handler *Handler
|
|
sc *servicesctrl.Control
|
|
persist db.Persist
|
|
|
|
managerChannelsList []*managerChannels
|
|
|
|
doneCh chan struct{}
|
|
enabled bool
|
|
}
|
|
|
|
func NewManager(persist db.Persist, sc *servicesctrl.Control) *Manager {
|
|
return &Manager{
|
|
handler: &Handler{},
|
|
sc: sc,
|
|
persist: persist,
|
|
doneCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (a *Manager) Close() {
|
|
close(a.doneCh)
|
|
}
|
|
|
|
func (a *Manager) Start(enabled bool) error {
|
|
a.enabled = enabled
|
|
if !a.enabled {
|
|
return nil
|
|
}
|
|
|
|
connsTicker, err := a.sc.Database()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.runTicker(connsTicker)
|
|
|
|
processingFunc := func(id string, managerChannel *managerChannels) error {
|
|
connsOuts, err := a.sc.Database()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.runProcessing("out_"+id, connsOuts, func(conns *utils.Connections) (uint64, error) {
|
|
return a.handler.processOutputs(true, processTypeOut, conns, a.persist)
|
|
}, managerChannel.ins)
|
|
|
|
connsIns, err := a.sc.Database()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.runProcessing("in_"+id, connsIns, func(conns *utils.Connections) (uint64, error) {
|
|
return a.handler.processOutputs(true, processTypeIn, conns, a.persist)
|
|
}, managerChannel.outs)
|
|
|
|
connsTxs, err := a.sc.Database()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.runProcessing("tx_"+id, connsTxs, func(conns *utils.Connections) (uint64, error) {
|
|
return a.handler.processTransactions(conns, a.persist)
|
|
}, managerChannel.txs)
|
|
return nil
|
|
}
|
|
|
|
for ipos := 0; ipos < 2; ipos++ {
|
|
managerChannels := newManagerChannels()
|
|
a.managerChannelsList = append(a.managerChannelsList, managerChannels)
|
|
err := processingFunc(fmt.Sprintf("%d", ipos), managerChannels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Manager) runTicker(conns *utils.Connections) {
|
|
a.sc.Logger().Info("start")
|
|
go func() {
|
|
defer func() {
|
|
a.sc.Logger().Info("stop")
|
|
}()
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
|
|
runEvent := func(conns *utils.Connections) {
|
|
icnt := 0
|
|
for ; icnt < retryProcessing; icnt++ {
|
|
cnt, err := a.handler.processOutputs(false, processTypeIn, conns, a.persist)
|
|
if utils.ErrIsLockError(err) {
|
|
icnt = 0
|
|
continue
|
|
}
|
|
if err != nil {
|
|
a.sc.Logger().Error("failed processing outputs",
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
if cnt < RowLimitValue {
|
|
break
|
|
}
|
|
icnt = 0
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
ticker.Stop()
|
|
err := conns.Close()
|
|
if err != nil {
|
|
a.sc.Logger().Warn("connection closed with error",
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
a.sc.Logger().Info("stop ticker")
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
runEvent(conns)
|
|
case <-a.doneCh:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Manager) runProcessing(id string, conns *utils.Connections, f func(conns *utils.Connections) (uint64, error), trigger chan struct{}) {
|
|
a.sc.Logger().Info("starting processing",
|
|
zap.String("id", id),
|
|
)
|
|
go func() {
|
|
defer func() {
|
|
err := conns.Close()
|
|
if err != nil {
|
|
a.sc.Logger().Warn("connection closed with error",
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
a.sc.Logger().Info("stopping processing",
|
|
zap.String("id", id),
|
|
)
|
|
}()
|
|
|
|
runEvent := func(conns *utils.Connections) {
|
|
icnt := 0
|
|
for ; icnt < retryProcessing; icnt++ {
|
|
cnt, err := f(conns)
|
|
if utils.ErrIsLockError(err) {
|
|
icnt = 0
|
|
continue
|
|
}
|
|
if err != nil {
|
|
a.sc.Logger().Error("failed processing",
|
|
zap.String("id", id),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
if cnt < RowLimitValue {
|
|
break
|
|
}
|
|
icnt = 0
|
|
}
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-trigger:
|
|
runEvent(conns)
|
|
case <-a.doneCh:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *Manager) Exec() {
|
|
if !a.enabled {
|
|
return
|
|
}
|
|
|
|
for _, managerChannels := range a.managerChannelsList {
|
|
select {
|
|
case managerChannels.ins <- struct{}{}:
|
|
default:
|
|
}
|
|
select {
|
|
case managerChannels.outs <- struct{}{}:
|
|
default:
|
|
}
|
|
select {
|
|
case managerChannels.txs <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
type Handler struct {
|
|
}
|
|
|
|
func outTable(typ processType) string {
|
|
tbl := ""
|
|
switch typ {
|
|
case processTypeOut:
|
|
tbl = db.TableOutputAddressAccumulateOut
|
|
case processTypeIn:
|
|
tbl = db.TableOutputAddressAccumulateIn
|
|
}
|
|
return tbl
|
|
}
|
|
|
|
func balanceTable(typ processType) string {
|
|
tbl := ""
|
|
switch typ {
|
|
case processTypeOut:
|
|
tbl = db.TableAccumulateBalancesReceived
|
|
case processTypeIn:
|
|
tbl = db.TableAccumulateBalancesSent
|
|
}
|
|
return tbl
|
|
}
|
|
|
|
func (a *Handler) processOutputsPre(outputProcessed bool, typ processType, session *dbr.Session) ([]*db.OutputAddressAccumulate, error) {
|
|
ctx, cancelCTX := context.WithTimeout(context.Background(), updTimeout)
|
|
defer cancelCTX()
|
|
|
|
var rowdata []*db.OutputAddressAccumulate
|
|
var err error
|
|
|
|
tbl := outTable(typ)
|
|
|
|
b := session.Select(
|
|
tbl+".id",
|
|
tbl+".output_id",
|
|
tbl+".address",
|
|
).
|
|
From(tbl).
|
|
Join("avm_outputs", tbl+".output_id = avm_outputs.id")
|
|
|
|
b.Where(tbl+".processed = ?", 0)
|
|
|
|
switch typ {
|
|
case processTypeOut:
|
|
case processTypeIn:
|
|
if outputProcessed {
|
|
b = b.
|
|
Where(tbl+".output_processed = ?", 1).
|
|
OrderAsc(tbl + ".output_processed")
|
|
} else {
|
|
b = b.
|
|
Join("avm_outputs_redeeming", tbl+".output_id = avm_outputs_redeeming.id ")
|
|
}
|
|
}
|
|
|
|
sb := b.
|
|
OrderAsc(tbl + ".processed").
|
|
OrderAsc(tbl + ".created_at").
|
|
Limit(RowLimitValue)
|
|
|
|
scq := session.Select(
|
|
"a.id",
|
|
"a.output_id",
|
|
"a.address",
|
|
"avm_outputs.transaction_id",
|
|
).From(sb.As("a")).
|
|
Join("avm_outputs", "a.output_id = avm_outputs.id")
|
|
|
|
switch typ {
|
|
case processTypeOut:
|
|
case processTypeIn:
|
|
scq = scq.
|
|
Join("avm_outputs_redeeming", "a.output_id = avm_outputs_redeeming.id ")
|
|
}
|
|
|
|
_, err = scq.
|
|
LoadContext(ctx, &rowdata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rand.Shuffle(len(rowdata), func(i, j int) { rowdata[i], rowdata[j] = rowdata[j], rowdata[i] })
|
|
|
|
return rowdata, nil
|
|
}
|
|
|
|
func (a *Handler) processOutputs(outputProcessed bool, typ processType, conns *utils.Connections, persist db.Persist) (uint64, error) {
|
|
job := conns.Stream().NewJob("accumulate-poll")
|
|
session := conns.DB().NewSessionForEventReceiver(job)
|
|
|
|
var err error
|
|
var rowdataAvail []*db.OutputAddressAccumulate
|
|
rowdataAvail, err = a.processOutputsPre(outputProcessed, typ, session)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
job = conns.Stream().NewJob("accumulate")
|
|
session = conns.DB().NewSessionForEventReceiver(job)
|
|
|
|
if len(rowdataAvail) > 0 {
|
|
trowsID := make([]string, 0, LockSize)
|
|
trows := make(map[string]*db.OutputAddressAccumulate)
|
|
for _, row := range rowdataAvail {
|
|
trows[row.ID] = row
|
|
trowsID = append(trowsID, row.ID)
|
|
if len(trowsID) > LockSize {
|
|
err := RepeatForLock(func() error { return a.processOutputsPost(trows, trowsID, typ, session, persist) })
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
trows = make(map[string]*db.OutputAddressAccumulate)
|
|
trowsID = make([]string, 0, LockSize)
|
|
}
|
|
}
|
|
|
|
if len(trowsID) > 0 {
|
|
err := RepeatForLock(func() error { return a.processOutputsPost(trows, trowsID, typ, session, persist) })
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return uint64(len(rowdataAvail)), nil
|
|
}
|
|
|
|
func (a *Handler) processOutputsPost(
|
|
workRows map[string]*db.OutputAddressAccumulate,
|
|
workRowsID []string,
|
|
typ processType,
|
|
session *dbr.Session,
|
|
persist db.Persist,
|
|
) error {
|
|
ctx, cancelCTX := context.WithTimeout(context.Background(), updTimeout)
|
|
defer cancelCTX()
|
|
|
|
tbl := outTable(typ)
|
|
|
|
var err error
|
|
var dbTx *dbr.Tx
|
|
dbTx, err = session.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dbTx.RollbackUnlessCommitted()
|
|
|
|
var rowdata []*db.OutputAddressAccumulate
|
|
|
|
_, err = dbTx.Select(
|
|
"id",
|
|
).
|
|
From(tbl).
|
|
Where("processed = ? and id in ?", 0, workRowsID).
|
|
Suffix("for update").
|
|
LoadContext(ctx, &rowdata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rowdata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, row := range rowdata {
|
|
err := a.processOutputsBase(ctx, typ, dbTx, persist, workRows[row.ID])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return dbTx.Commit()
|
|
}
|
|
|
|
func (a *Handler) processOutputsBase(
|
|
ctx context.Context,
|
|
typ processType,
|
|
dbTx *dbr.Tx,
|
|
persist db.Persist,
|
|
row *db.OutputAddressAccumulate,
|
|
) error {
|
|
var err error
|
|
|
|
tbl := outTable(typ)
|
|
balancetbl := balanceTable(typ)
|
|
|
|
_, err = dbTx.Update(tbl).
|
|
Set("processed", 1).
|
|
Where("id=?", row.ID).
|
|
ExecContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var balances []*db.AccumulateBalancesAmount
|
|
|
|
_, err = dbTx.Select(
|
|
"avm_outputs.chain_id",
|
|
"avm_output_addresses.address",
|
|
"avm_outputs.asset_id",
|
|
"sum(avm_outputs.amount) as total_amount",
|
|
).From("avm_outputs").
|
|
Join("avm_output_addresses", "avm_outputs.id = avm_output_addresses.output_id").
|
|
Where("avm_outputs.id=? and avm_output_addresses.address=?", row.OutputID, row.Address).
|
|
GroupBy("chain_id", "avm_output_addresses.address", "avm_outputs.asset_id").
|
|
LoadContext(ctx, &balances)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(balances) == 0 {
|
|
return fmt.Errorf("invalid balance")
|
|
}
|
|
|
|
for _, b := range balances {
|
|
// add any missing txs rows.
|
|
outputsTxsAccumulate := &db.OutputTxsAccumulate{
|
|
ChainID: b.ChainID,
|
|
AssetID: b.AssetID,
|
|
Address: b.Address,
|
|
TransactionID: row.TransactionID,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
outputsTxsAccumulate.ComputeID()
|
|
err = persist.InsertOutputTxsAccumulate(ctx, dbTx, outputsTxsAccumulate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.UpdatedAt = time.Unix(1, 0)
|
|
b.ComputeID()
|
|
|
|
switch typ {
|
|
case processTypeOut:
|
|
err = persist.InsertAccumulateBalancesReceived(ctx, dbTx, b)
|
|
case processTypeIn:
|
|
err = persist.InsertAccumulateBalancesSent(ctx, dbTx, b)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var balancesLocked []*db.AccumulateBalancesAmount
|
|
|
|
_, err = dbTx.Select("id").
|
|
From(balancetbl).
|
|
Where("id = ?", b.ID).
|
|
Suffix("for update").
|
|
LoadContext(ctx, &balancesLocked)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// we didn't lock the row
|
|
if len(balancesLocked) == 0 {
|
|
return fmt.Errorf("balancesLocked failed")
|
|
}
|
|
brow := balancesLocked[0]
|
|
if brow.ID != b.ID {
|
|
return fmt.Errorf("balancesLocked failed")
|
|
}
|
|
|
|
_, err = dbTx.UpdateBySql("update "+balancetbl+" "+
|
|
"set "+
|
|
"utxo_count = utxo_count+1, "+
|
|
"total_amount = total_amount+"+b.TotalAmount+", "+
|
|
"updated_at = ? "+
|
|
"where id=? "+
|
|
"", time.Now().UTC(), b.ID).
|
|
ExecContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Handler) processTransactionsPre(session *dbr.Session) ([]*db.OutputTxsAccumulate, error) {
|
|
ctx, cancelCTX := context.WithTimeout(context.Background(), updTimeout)
|
|
defer cancelCTX()
|
|
|
|
var err error
|
|
var rowdata []*db.OutputTxsAccumulate
|
|
|
|
b := session.Select(
|
|
"id",
|
|
"chain_id",
|
|
"asset_id",
|
|
"address",
|
|
"transaction_id",
|
|
"processed",
|
|
).
|
|
From(db.TableOutputTxsAccumulate)
|
|
|
|
_, err = b.
|
|
Where("processed = ?", 0).
|
|
OrderAsc("processed").
|
|
OrderAsc("created_at").
|
|
Limit(RowLimitValue).
|
|
LoadContext(ctx, &rowdata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rand.Shuffle(len(rowdata), func(i, j int) { rowdata[i], rowdata[j] = rowdata[j], rowdata[i] })
|
|
|
|
return rowdata, nil
|
|
}
|
|
|
|
func (a *Handler) processTransactions(conns *utils.Connections, persist db.Persist) (uint64, error) {
|
|
job := conns.Stream().NewJob("accumulate-poll")
|
|
session := conns.DB().NewSessionForEventReceiver(job)
|
|
|
|
var err error
|
|
var rowdataAvail []*db.OutputTxsAccumulate
|
|
rowdataAvail, err = a.processTransactionsPre(session)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
job = conns.Stream().NewJob("accumulate")
|
|
session = conns.DB().NewSessionForEventReceiver(job)
|
|
|
|
if len(rowdataAvail) > 0 {
|
|
trowsID := make([]string, 0, LockSize)
|
|
trows := make(map[string]*db.OutputTxsAccumulate)
|
|
for _, row := range rowdataAvail {
|
|
trows[row.ID] = row
|
|
trowsID = append(trowsID, row.ID)
|
|
if len(trowsID) > LockSize {
|
|
err := RepeatForLock(func() error { return a.processTransactionsPost(trows, trowsID, session, persist) })
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
trows = make(map[string]*db.OutputTxsAccumulate)
|
|
trowsID = make([]string, 0, LockSize)
|
|
}
|
|
}
|
|
|
|
if len(trowsID) > 0 {
|
|
err := RepeatForLock(func() error { return a.processTransactionsPost(trows, trowsID, session, persist) })
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return uint64(len(rowdataAvail)), nil
|
|
}
|
|
|
|
func (a *Handler) processTransactionsPost(
|
|
workRows map[string]*db.OutputTxsAccumulate,
|
|
workRowsID []string,
|
|
session *dbr.Session,
|
|
persist db.Persist,
|
|
) error {
|
|
ctx, cancelCTX := context.WithTimeout(context.Background(), updTimeout)
|
|
defer cancelCTX()
|
|
|
|
var err error
|
|
var dbTx *dbr.Tx
|
|
dbTx, err = session.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dbTx.RollbackUnlessCommitted()
|
|
|
|
var rowdata []*db.OutputTxsAccumulate
|
|
|
|
_, err = dbTx.Select(
|
|
"id",
|
|
).
|
|
From(db.TableOutputTxsAccumulate).
|
|
Where("processed = ? and id in ?", 0, workRowsID).
|
|
Suffix("for update").
|
|
LoadContext(ctx, &rowdata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rowdata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, row := range rowdata {
|
|
err := a.processTransactionsBase(ctx, dbTx, persist, workRows[row.ID])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return dbTx.Commit()
|
|
}
|
|
|
|
func (a *Handler) processTransactionsBase(
|
|
ctx context.Context,
|
|
dbTx *dbr.Tx,
|
|
persist db.Persist,
|
|
row *db.OutputTxsAccumulate,
|
|
) error {
|
|
var err error
|
|
|
|
_, err = dbTx.Update(db.TableOutputTxsAccumulate).
|
|
Set("processed", 1).
|
|
Where("id = ?", row.ID).
|
|
ExecContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b := &db.AccumulateBalancesTransactions{
|
|
ChainID: row.ChainID,
|
|
AssetID: row.AssetID,
|
|
Address: row.Address,
|
|
UpdatedAt: time.Unix(1, 0),
|
|
}
|
|
b.ComputeID()
|
|
err = persist.InsertAccumulateBalancesTransactions(ctx, dbTx, b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var balancesLocked []*db.AccumulateBalancesTransactions
|
|
_, err = dbTx.Select("id").
|
|
From(db.TableAccumulateBalancesTransactions).
|
|
Where("id = ?", b.ID).
|
|
Suffix("for update").
|
|
LoadContext(ctx, &balancesLocked)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// we didn't lock the row
|
|
if len(balancesLocked) == 0 {
|
|
return fmt.Errorf("balancesLocked failed")
|
|
}
|
|
brow := balancesLocked[0]
|
|
if brow.ID != b.ID {
|
|
return fmt.Errorf("balancesLocked failed")
|
|
}
|
|
|
|
_, err = dbTx.UpdateBySql("update "+db.TableAccumulateBalancesTransactions+" "+
|
|
"set "+
|
|
"transaction_count = transaction_count+1, "+
|
|
"updated_at = ? "+
|
|
"where id=? "+
|
|
"", time.Now().UTC(), b.ID).
|
|
ExecContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func RepeatForLock(f func() error) error {
|
|
var err error
|
|
for {
|
|
err = f()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if utils.ErrIsLockError(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|