github.com/klaytn/klaytn@v1.12.1/datasync/dbsyncer/dbsync.go (about)

     1  // Copyright 2019 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The klaytn library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package dbsyncer
    18  
    19  import (
    20  	"context"
    21  	"database/sql"
    22  	"strings"
    23  	"time"
    24  
    25  	_ "github.com/go-sql-driver/mysql"
    26  	"github.com/klaytn/klaytn/blockchain"
    27  	"github.com/klaytn/klaytn/blockchain/types"
    28  	"github.com/klaytn/klaytn/event"
    29  	"github.com/klaytn/klaytn/log"
    30  	"github.com/klaytn/klaytn/networks/p2p"
    31  	"github.com/klaytn/klaytn/networks/rpc"
    32  	"github.com/klaytn/klaytn/node"
    33  	"github.com/klaytn/klaytn/work"
    34  	"github.com/pkg/errors"
    35  )
    36  
    37  var logger = log.NewModuleLogger(log.Node)
    38  
    39  type DBSyncer struct {
    40  	cfg        *DBConfig
    41  	dataSource string
    42  
    43  	blockchain *blockchain.BlockChain
    44  
    45  	// chain event
    46  	chainCh     chan blockchain.ChainEvent
    47  	chainHeadCh chan blockchain.ChainHeadEvent
    48  	chainSub    event.Subscription
    49  	logsCh      chan []*types.Log
    50  	logsSub     event.Subscription
    51  
    52  	ctx  context.Context
    53  	stop context.CancelFunc
    54  	db   *sql.DB
    55  
    56  	logMode bool
    57  
    58  	blockInsertQuery     string
    59  	txInsertQuery        string
    60  	summaryInsertQuery   string
    61  	txHashMapInsertQuery string
    62  
    63  	HandleBlock func(block *types.Block) error
    64  	queryEngine *QueryEngine
    65  
    66  	bulkInsertSize int
    67  
    68  	eventMode string
    69  
    70  	maxBlockDiff uint64
    71  }
    72  
    73  func NewDBSyncer(ctx *node.ServiceContext, cfg *DBConfig) (*DBSyncer, error) {
    74  	logger.Info("initialize DBSyncer", "db.host",
    75  		cfg.DBHost, "db.port", cfg.DBPort, "db.name", cfg.DBName, "db.user", cfg.DBUser, "db.max.idle",
    76  		cfg.MaxIdleConns, "db.password", cfg.DBPassword, "db.max.open", cfg.MaxOpenConns, "db.max.lifetime",
    77  		cfg.ConnMaxLifetime, "block.ch.size", cfg.BlockChannelSize, "mode", cfg.Mode, "genquery.th",
    78  		cfg.GenQueryThread, "insert.th", cfg.InsertThread, "bulk.size", cfg.BulkInsertSize, "event.mode",
    79  		cfg.EventMode, "max.block.diff", cfg.MaxBlockDiff)
    80  
    81  	if cfg.DBHost == "" {
    82  		return nil, errors.New("db config must be set (db.host)")
    83  	} else if cfg.DBName == "" {
    84  		return nil, errors.New("db config must be set (db.name)")
    85  	} else if cfg.DBUser == "" {
    86  		return nil, errors.New("db config must be set (db.user)")
    87  	} else if cfg.DBPassword == "" {
    88  		return nil, errors.New("db config must be set (db.password)")
    89  	}
    90  
    91  	return &DBSyncer{
    92  		cfg:            cfg,
    93  		logMode:        cfg.EnabledLogMode,
    94  		bulkInsertSize: cfg.BulkInsertSize,
    95  		eventMode:      cfg.EventMode,
    96  		maxBlockDiff:   cfg.MaxBlockDiff,
    97  	}, nil
    98  }
    99  
   100  func (ds *DBSyncer) Protocols() []p2p.Protocol {
   101  	return []p2p.Protocol{}
   102  }
   103  
   104  func (ds *DBSyncer) APIs() []rpc.API {
   105  	return []rpc.API{}
   106  }
   107  
   108  func (ds *DBSyncer) Start(server p2p.Server) error {
   109  	ds.dataSource = ds.cfg.DBUser + ":" + ds.cfg.DBPassword + "@tcp(" + ds.cfg.DBHost + ":" +
   110  		ds.cfg.DBPort + ")/" + ds.cfg.DBName + "?writeTimeout=10s&timeout=10s"
   111  
   112  	db, err := sql.Open("mysql", ds.dataSource)
   113  	if err != nil {
   114  		logger.Error("fail to connect database", "target", ds.dataSource)
   115  		return err
   116  	}
   117  	ds.db = db
   118  	ds.db.SetMaxIdleConns(ds.cfg.MaxIdleConns)
   119  	ds.db.SetConnMaxLifetime(ds.cfg.ConnMaxLifetime)
   120  	ds.db.SetMaxOpenConns(ds.cfg.MaxOpenConns)
   121  
   122  	// initialize context
   123  	ds.ctx, ds.stop = context.WithCancel(context.Background())
   124  
   125  	// query
   126  	ds.blockInsertQuery = "INSERT INTO " + ds.cfg.DBName + ".block " + "(totalTx, " +
   127  		"committee, gasUsed, gasPrice, hash, " +
   128  		"number, parentHash, proposer, reward, size, " +
   129  		"timestamp, timestampFoS)" + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
   130  
   131  	ds.txInsertQuery = "INSERT INTO " + ds.cfg.DBName + ".transaction " + "(id, blockHash, blockNumber, " +
   132  		"contractAddress, `from`, gas, gasPrice, gasUsed, input, nonce, status, `to`, " +
   133  		"timestamp, txHash, type, value, feePayer, feeRatio, senderTxHash) VALUES "
   134  
   135  	ds.summaryInsertQuery = "INSERT INTO " + ds.cfg.DBName + ".account_summary " + "(address, type, " +
   136  		"creator, created_tx, hra) VALUES "
   137  
   138  	ds.txHashMapInsertQuery = "INSERT INTO " + ds.cfg.DBName + ".sendertxhash_map " + "(senderTxHash, txHash) VALUES "
   139  
   140  	if ds.cfg.Mode == "single" {
   141  		ds.HandleBlock = ds.HandleChainEvent
   142  	} else if ds.cfg.Mode == "multi" {
   143  		ds.HandleBlock = ds.HandleChainEventParallel
   144  		ds.queryEngine = newQueryEngine(ds, ds.cfg.GenQueryThread, ds.cfg.InsertThread)
   145  	} else if ds.cfg.Mode == "context" {
   146  		ds.HandleBlock = ds.HandleChainEventContext
   147  	} else {
   148  		ds.HandleBlock = ds.HandleChainEventParallel
   149  		ds.queryEngine = newQueryEngine(ds, ds.cfg.GenQueryThread, ds.cfg.InsertThread)
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func (ds *DBSyncer) Stop() error {
   156  	if ds.db != nil {
   157  		if err := ds.db.Close(); err != nil {
   158  			logger.Error("fail to close db", "err", err)
   159  		}
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func (ds *DBSyncer) Components() []interface{} {
   166  	return nil
   167  }
   168  
   169  func (ds *DBSyncer) SetComponents(components []interface{}) {
   170  	for _, component := range components {
   171  		switch v := component.(type) {
   172  		case *blockchain.BlockChain:
   173  			ds.blockchain = v
   174  			// event from core-service
   175  			if ds.eventMode == BLOCK_MODE {
   176  				// handle all blocks when many blocks create
   177  				ds.chainCh = make(chan blockchain.ChainEvent, ds.cfg.BlockChannelSize)
   178  				ds.chainSub = ds.blockchain.SubscribeChainEvent(ds.chainCh)
   179  				// eventMode == "head"
   180  			} else if ds.eventMode == HEAD_MODE {
   181  				// handle last block when many blocks create
   182  				ds.chainHeadCh = make(chan blockchain.ChainHeadEvent, ds.cfg.BlockChannelSize)
   183  				ds.chainSub = ds.blockchain.SubscribeChainHeadEvent(ds.chainHeadCh)
   184  			} else {
   185  				logger.Error("unknown event.mode (block,head)", "current mode", ds.eventMode)
   186  			}
   187  			// ds.logsSub = ds.blockchain.SubscribeLogsEvent(ds.logsCh)
   188  		case *blockchain.TxPool:
   189  		case *work.Miner:
   190  		}
   191  	}
   192  
   193  	go ds.loop()
   194  }
   195  
   196  func (ds *DBSyncer) loop() {
   197  	report := time.NewTicker(1 * time.Minute)
   198  	defer report.Stop()
   199  
   200  	// Keep waiting for and reacting to the various events
   201  	for {
   202  		select {
   203  		// Handle ChainEvent
   204  		case ev := <-ds.chainCh:
   205  			if ev.Block != nil {
   206  				ds.HandleDiffBlock(ev.Block)
   207  			} else {
   208  				logger.Error("dbsyncer block event is nil")
   209  			}
   210  		case ev := <-ds.chainHeadCh:
   211  			if ev.Block != nil {
   212  				ds.HandleDiffBlock(ev.Block)
   213  			} else {
   214  				logger.Error("dbsyncer block event is nil")
   215  			}
   216  		case <-report.C:
   217  			// check db health
   218  			go ds.Ping()
   219  		case err := <-ds.chainSub.Err():
   220  			if err != nil {
   221  				logger.Error("dbsyncer block subscription ", "err", err)
   222  			}
   223  			return
   224  		}
   225  	}
   226  }
   227  
   228  func (ds *DBSyncer) HandleDiffBlock(block *types.Block) {
   229  	diff := ds.blockchain.CurrentBlock().NumberU64() - block.NumberU64()
   230  
   231  	if ds.maxBlockDiff > 0 && diff > ds.maxBlockDiff {
   232  		logger.Info("there are many block number difference (skip block)", "diff", diff, "skip-block", block.NumberU64())
   233  	} else {
   234  		if err := ds.HandleBlock(block); err != nil {
   235  			logger.Error("dbsyncer block event", "block", block.Number(), "err", err)
   236  		}
   237  	}
   238  }
   239  
   240  func (ds *DBSyncer) Ping() {
   241  	logger.Info("check database", "target", ds.dataSource)
   242  	ctx, cancel := context.WithTimeout(ds.ctx, 10*time.Second)
   243  	defer cancel()
   244  
   245  	err := ds.db.PingContext(ctx)
   246  	if err != nil {
   247  		logger.Error("database down", "target", ds.dataSource, "err", err)
   248  	}
   249  }
   250  
   251  func (ds *DBSyncer) HandleChainEvent(block *types.Block) error {
   252  	logger.Info("dbsyncer HandleChainEvent", "number", block.Number(), "txs", block.Transactions().Len())
   253  	startblock := time.Now()
   254  
   255  	if err := ds.syncBlockHeader(block); err != nil {
   256  		logger.Error("fail to sync block", "block", block.Number(), "err", err)
   257  		return err
   258  	}
   259  
   260  	blocktime := time.Since(startblock)
   261  	starttx := time.Now()
   262  
   263  	if block.Transactions().Len() > 0 {
   264  		if err := ds.SyncTransactions(block); err != nil {
   265  			logger.Error("fail to sync transaction", "block", block.Number(), "err", err)
   266  			return err
   267  		}
   268  	}
   269  
   270  	txtime := time.Since(starttx)
   271  	totalTime := time.Since(startblock)
   272  	if ds.logMode {
   273  		logger.Info("dbsync time", "number", block.Number(), "block", blocktime, "txs", txtime, "total", totalTime)
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  func (ds *DBSyncer) syncBlockHeader(block *types.Block) error {
   280  	proposerAddr, committeeAddrs, err := getProposerAndValidatorsFromBlock(block)
   281  
   282  	totalTx := block.Transactions().Len()
   283  	committee := strings.ToLower(committeeAddrs)
   284  	gasUsed := block.Header().GasUsed
   285  	gasPrice := ds.blockchain.Config().UnitPrice
   286  	hash := block.Header().Hash().Hex()
   287  	number := block.Header().Number.Uint64()
   288  	parentHash := block.Header().ParentHash.Hex()
   289  	proposer := strings.ToLower(proposerAddr)
   290  	reward := block.Header().Rewardbase.Hex()
   291  	size := block.Size()
   292  	timestamp := block.Header().Time.String()
   293  	timestampFos := block.Header().TimeFoS
   294  
   295  	stmtIns, err := ds.db.Prepare(ds.blockInsertQuery)
   296  	if err != nil {
   297  		logger.Error("fail to prepare (block)", "query", ds.blockInsertQuery)
   298  		return err
   299  	}
   300  
   301  	defer func() {
   302  		if err := stmtIns.Close(); err != nil {
   303  			logger.Error("fail to close stmt", "err", err)
   304  		}
   305  	}()
   306  
   307  	if _, err := stmtIns.Exec(totalTx, committee, gasUsed, gasPrice, hash,
   308  		number, parentHash, proposer, reward, size, timestamp, timestampFos); err != nil {
   309  		logger.Error("fail to insert DB (block)", "number", block.Number(), "err", err)
   310  		return err
   311  	}
   312  
   313  	return nil
   314  }
   315  
   316  func (ds *DBSyncer) SyncTransactions(block *types.Block) error {
   317  	txKey := block.NumberU64() * TX_KEY_FACTOR
   318  	txStr, vals, insertCount := ds.resetTxParameter()
   319  	summaryStr, summaryVals, summaryInsertCount := ds.resetSummaryParameter()
   320  	txMapStr, txMapVals, txMapInsertCount := ds.resetTxMapParameter()
   321  
   322  	receipts := ds.blockchain.GetReceiptsByBlockHash(block.Hash())
   323  
   324  	for index, tx := range block.Transactions() {
   325  		txKey += uint64(index)
   326  		cols, val, txMapArg, summaryArg, err := MakeTxDBRow(block, txKey, tx, receipts[index])
   327  		if err != nil {
   328  			return err
   329  		}
   330  
   331  		txStr += cols + ","
   332  		vals = append(vals, val...)
   333  		insertCount++
   334  
   335  		if insertCount >= ds.bulkInsertSize {
   336  			if err := ds.bulkInsert(txStr, vals, block.NumberU64(), insertCount); err != nil {
   337  				return err
   338  			}
   339  			txStr, vals, insertCount = ds.resetTxParameter()
   340  		}
   341  
   342  		scols, sval, count, err := MakeSummaryDBRow(summaryArg)
   343  		if err != nil {
   344  			return err
   345  		}
   346  
   347  		if count == 1 {
   348  			summaryStr += scols + ","
   349  			summaryVals = append(summaryVals, sval...)
   350  			summaryInsertCount++
   351  		}
   352  
   353  		if summaryInsertCount >= ds.bulkInsertSize {
   354  			if err := ds.bulkInsert(summaryStr, summaryVals, block.NumberU64(), summaryInsertCount); err != nil {
   355  				return err
   356  			}
   357  			summaryStr, summaryVals, summaryInsertCount = ds.resetTxParameter()
   358  		}
   359  
   360  		tcols, tval, tcount, err := MakeTxMappingRow(txMapArg)
   361  		if err != nil {
   362  			return err
   363  		}
   364  
   365  		if tcount == 1 {
   366  			txMapStr += tcols + ","
   367  			txMapVals = append(txMapVals, tval...)
   368  			txMapInsertCount++
   369  		}
   370  
   371  		if txMapInsertCount >= ds.bulkInsertSize {
   372  			if err := ds.bulkInsert(txMapStr, txMapVals, block.NumberU64(), txMapInsertCount); err != nil {
   373  				return err
   374  			}
   375  			txMapStr, txMapVals, txMapInsertCount = ds.resetTxMapParameter()
   376  		}
   377  	}
   378  
   379  	if insertCount > 0 {
   380  		if err := ds.bulkInsert(txStr, vals, block.NumberU64(), insertCount); err != nil {
   381  			return err
   382  		}
   383  	}
   384  
   385  	if summaryInsertCount > 0 {
   386  		if err := ds.bulkInsert(summaryStr, summaryVals, block.NumberU64(), summaryInsertCount); err != nil {
   387  			return err
   388  		}
   389  	}
   390  
   391  	if txMapInsertCount > 0 {
   392  		if err := ds.bulkInsert(txMapStr, txMapVals, block.NumberU64(), txMapInsertCount); err != nil {
   393  			return err
   394  		}
   395  	}
   396  
   397  	return nil
   398  }
   399  
   400  func (ds *DBSyncer) resetTxParameter() (txStr string, vals []interface{}, insertCount int) {
   401  	txStr = ds.txInsertQuery
   402  	vals = []interface{}{}
   403  	insertCount = 0
   404  
   405  	return txStr, vals, insertCount
   406  }
   407  
   408  func (ds *DBSyncer) resetSummaryParameter() (summaryStr string, vals []interface{}, insertCount int) {
   409  	summaryStr = ds.summaryInsertQuery
   410  	vals = []interface{}{}
   411  	insertCount = 0
   412  
   413  	return summaryStr, vals, insertCount
   414  }
   415  
   416  func (ds *DBSyncer) resetTxMapParameter() (txMapStr string, vals []interface{}, insertCount int) {
   417  	txMapStr = ds.txHashMapInsertQuery
   418  	vals = []interface{}{}
   419  	insertCount = 0
   420  
   421  	return txMapStr, vals, insertCount
   422  }
   423  
   424  func (ds *DBSyncer) bulkInsert(sqlStr string, vals []interface{}, blockNumber uint64, insertCount int) error {
   425  	start := time.Now()
   426  	// trim the last
   427  	sqlStr = sqlStr[0 : len(sqlStr)-1]
   428  
   429  	stmtTxs, err := ds.db.Prepare(sqlStr)
   430  	if err != nil {
   431  		logger.Error("fail to create prepare", "sql", sqlStr, "err", err)
   432  		return err
   433  	}
   434  
   435  	if _, err := stmtTxs.Exec(vals...); err != nil {
   436  		logger.Error("fail to insert DB (tx)", "number", blockNumber, "err", err)
   437  		return err
   438  	}
   439  	defer func() {
   440  		if err := stmtTxs.Close(); err != nil {
   441  			logger.Error("fail to close stmt", "err", err)
   442  		}
   443  	}()
   444  
   445  	if ds.logMode {
   446  		txTime := time.Since(start)
   447  		logger.Info("TX/BLOCK INSERT", "block", blockNumber, "counts", insertCount, "time", txTime)
   448  	}
   449  
   450  	return nil
   451  }
   452  
   453  func (ds *DBSyncer) HandleLogsEvent(logs []*types.Log) error {
   454  	return nil
   455  }