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 }