github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/blockbook.go (about) 1 package main 2 3 import ( 4 "blockbook/api" 5 "blockbook/bchain" 6 "blockbook/bchain/coins" 7 "blockbook/common" 8 "blockbook/db" 9 "blockbook/server" 10 "context" 11 "flag" 12 "log" 13 "math/rand" 14 "net/http" 15 _ "net/http/pprof" 16 "os" 17 "os/signal" 18 "strings" 19 "sync/atomic" 20 "syscall" 21 "time" 22 23 "github.com/golang/glog" 24 "github.com/juju/errors" 25 ) 26 27 // debounce too close requests for resync 28 const debounceResyncIndexMs = 1009 29 30 // debounce too close requests for resync mempool (ZeroMQ sends message for each tx, when new block there are many transactions) 31 const debounceResyncMempoolMs = 1009 32 33 // store internal state about once every minute 34 const storeInternalStatePeriodMs = 59699 35 36 var ( 37 blockchain = flag.String("blockchaincfg", "", "path to blockchain RPC service configuration json file") 38 39 dbPath = flag.String("datadir", "./data", "path to database directory") 40 dbCache = flag.Int("dbcache", 1<<29, "size of the rocksdb cache") 41 dbMaxOpenFiles = flag.Int("dbmaxopenfiles", 1<<14, "max open files by rocksdb") 42 43 blockFrom = flag.Int("blockheight", -1, "height of the starting block") 44 blockUntil = flag.Int("blockuntil", -1, "height of the final block") 45 rollbackHeight = flag.Int("rollback", -1, "rollback to the given height and quit") 46 47 queryAddress = flag.String("address", "", "query contents of this address") 48 49 synchronize = flag.Bool("sync", false, "synchronizes until tip, if together with zeromq, keeps index synchronized") 50 repair = flag.Bool("repair", false, "repair the database") 51 prof = flag.String("prof", "", "http server binding [address]:port of the interface to profiling data /debug/pprof/ (default no profiling)") 52 53 syncChunk = flag.Int("chunk", 100, "block chunk size for processing in bulk mode") 54 syncWorkers = flag.Int("workers", 8, "number of workers to process blocks in bulk mode") 55 dryRun = flag.Bool("dryrun", false, "do not index blocks, only download") 56 57 debugMode = flag.Bool("debug", false, "debug mode, return more verbose errors, reload templates on each request") 58 59 internalBinding = flag.String("internal", "", "internal http server binding [address]:port, (default no internal server)") 60 61 publicBinding = flag.String("public", "", "public http server binding [address]:port[/path] (default no public server)") 62 63 certFiles = flag.String("certfile", "", "to enable SSL specify path to certificate files without extension, expecting <certfile>.crt and <certfile>.key (default no SSL)") 64 65 explorerURL = flag.String("explorer", "", "address of blockchain explorer") 66 67 noTxCache = flag.Bool("notxcache", false, "disable tx cache") 68 69 computeColumnStats = flag.Bool("computedbstats", false, "compute column stats and exit") 70 71 // resync index at least each resyncIndexPeriodMs (could be more often if invoked by message from ZeroMQ) 72 resyncIndexPeriodMs = flag.Int("resyncindexperiod", 935093, "resync index period in milliseconds") 73 74 // resync mempool at least each resyncMempoolPeriodMs (could be more often if invoked by message from ZeroMQ) 75 resyncMempoolPeriodMs = flag.Int("resyncmempoolperiod", 60017, "resync mempool period in milliseconds") 76 ) 77 78 var ( 79 chanSyncIndex = make(chan struct{}) 80 chanSyncMempool = make(chan struct{}) 81 chanStoreInternalState = make(chan struct{}) 82 chanSyncIndexDone = make(chan struct{}) 83 chanSyncMempoolDone = make(chan struct{}) 84 chanStoreInternalStateDone = make(chan struct{}) 85 chain bchain.BlockChain 86 index *db.RocksDB 87 txCache *db.TxCache 88 metrics *common.Metrics 89 syncWorker *db.SyncWorker 90 internalState *common.InternalState 91 callbacksOnNewBlock []bchain.OnNewBlockFunc 92 callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc 93 chanOsSignal chan os.Signal 94 inShutdown int32 95 ) 96 97 func init() { 98 glog.MaxSize = 1024 * 1024 * 8 99 glog.CopyStandardLogTo("INFO") 100 } 101 102 func getBlockChainWithRetry(coin string, configfile string, pushHandler func(bchain.NotificationType), metrics *common.Metrics, seconds int) (bchain.BlockChain, error) { 103 var chain bchain.BlockChain 104 var err error 105 timer := time.NewTimer(time.Second) 106 for i := 0; ; i++ { 107 if chain, err = coins.NewBlockChain(coin, configfile, pushHandler, metrics); err != nil { 108 if i < seconds { 109 glog.Error("rpc: ", err, " Retrying...") 110 select { 111 case <-chanOsSignal: 112 return nil, errors.New("Interrupted") 113 case <-timer.C: 114 timer.Reset(time.Second) 115 continue 116 } 117 } else { 118 return nil, err 119 } 120 } 121 return chain, nil 122 } 123 } 124 125 func main() { 126 flag.Parse() 127 128 defer glog.Flush() 129 130 rand.Seed(time.Now().UTC().UnixNano()) 131 132 chanOsSignal = make(chan os.Signal, 1) 133 signal.Notify(chanOsSignal, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 134 135 glog.Infof("Blockbook: %+v, debug mode %v", common.GetVersionInfo(), *debugMode) 136 137 if *prof != "" { 138 go func() { 139 log.Println(http.ListenAndServe(*prof, nil)) 140 }() 141 } 142 143 if *repair { 144 if err := db.RepairRocksDB(*dbPath); err != nil { 145 glog.Fatalf("RepairRocksDB %s: %v", *dbPath, err) 146 } 147 return 148 } 149 150 if *blockchain == "" { 151 glog.Fatal("Missing blockchaincfg configuration parameter") 152 } 153 154 coin, coinShortcut, coinLabel, err := coins.GetCoinNameFromConfig(*blockchain) 155 if err != nil { 156 glog.Fatal("config: ", err) 157 } 158 159 // gspt.SetProcTitle("blockbook-" + normalizeName(coin)) 160 161 metrics, err = common.GetMetrics(coin) 162 if err != nil { 163 glog.Fatal("metrics: ", err) 164 } 165 166 if chain, err = getBlockChainWithRetry(coin, *blockchain, pushSynchronizationHandler, metrics, 60); err != nil { 167 glog.Fatal("rpc: ", err) 168 } 169 170 index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics) 171 if err != nil { 172 glog.Fatal("rocksDB: ", err) 173 } 174 defer index.Close() 175 176 internalState, err = newInternalState(coin, coinShortcut, coinLabel, index) 177 if err != nil { 178 glog.Error("internalState: ", err) 179 return 180 } 181 index.SetInternalState(internalState) 182 if internalState.DbState != common.DbStateClosed { 183 if internalState.DbState == common.DbStateInconsistent { 184 glog.Error("internalState: database is in inconsistent state and cannot be used") 185 return 186 } 187 glog.Warning("internalState: database was left in open state, possibly previous ungraceful shutdown") 188 } 189 190 if *computeColumnStats { 191 internalState.DbState = common.DbStateOpen 192 err = index.ComputeInternalStateColumnStats(chanOsSignal) 193 if err != nil { 194 glog.Error("internalState: ", err) 195 } 196 glog.Info("DB size on disk: ", index.DatabaseSizeOnDisk(), ", DB size as computed: ", internalState.DBSizeTotal()) 197 return 198 } 199 200 syncWorker, err = db.NewSyncWorker(index, chain, *syncWorkers, *syncChunk, *blockFrom, *dryRun, chanOsSignal, metrics, internalState) 201 if err != nil { 202 glog.Fatalf("NewSyncWorker %v", err) 203 } 204 205 // set the DbState to open at this moment, after all important workers are initialized 206 internalState.DbState = common.DbStateOpen 207 err = index.StoreInternalState(internalState) 208 if err != nil { 209 glog.Fatal("internalState: ", err) 210 } 211 212 if *rollbackHeight >= 0 { 213 bestHeight, bestHash, err := index.GetBestBlock() 214 if err != nil { 215 glog.Error("rollbackHeight: ", err) 216 return 217 } 218 if uint32(*rollbackHeight) > bestHeight { 219 glog.Infof("nothing to rollback, rollbackHeight %d, bestHeight: %d", *rollbackHeight, bestHeight) 220 } else { 221 hashes := []string{bestHash} 222 for height := bestHeight - 1; height >= uint32(*rollbackHeight); height-- { 223 hash, err := index.GetBlockHash(height) 224 if err != nil { 225 glog.Error("rollbackHeight: ", err) 226 return 227 } 228 hashes = append(hashes, hash) 229 } 230 err = syncWorker.DisconnectBlocks(uint32(*rollbackHeight), bestHeight, hashes) 231 if err != nil { 232 glog.Error("rollbackHeight: ", err) 233 return 234 } 235 } 236 return 237 } 238 239 if txCache, err = db.NewTxCache(index, chain, metrics, internalState, !*noTxCache); err != nil { 240 glog.Error("txCache ", err) 241 return 242 } 243 244 // report BlockbookAppInfo metric, only log possible error 245 if err = blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { 246 glog.Error("blockbookAppInfoMetric ", err) 247 } 248 249 var internalServer *server.InternalServer 250 if *internalBinding != "" { 251 internalServer, err = server.NewInternalServer(*internalBinding, *certFiles, index, chain, txCache, internalState) 252 if err != nil { 253 glog.Error("https: ", err) 254 return 255 } 256 go func() { 257 err = internalServer.Run() 258 if err != nil { 259 if err.Error() == "http: Server closed" { 260 glog.Info("internal server: closed") 261 } else { 262 glog.Error(err) 263 return 264 } 265 } 266 }() 267 } 268 269 var publicServer *server.PublicServer 270 if *publicBinding != "" { 271 // start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface 272 publicServer, err = server.NewPublicServer(*publicBinding, *certFiles, index, chain, txCache, *explorerURL, metrics, internalState, *debugMode) 273 if err != nil { 274 glog.Error("socketio: ", err) 275 return 276 } 277 go func() { 278 err = publicServer.Run() 279 if err != nil { 280 if err.Error() == "http: Server closed" { 281 glog.Info("public server: closed") 282 } else { 283 glog.Error(err) 284 return 285 } 286 } 287 }() 288 callbacksOnNewBlock = append(callbacksOnNewBlock, publicServer.OnNewBlock) 289 callbacksOnNewTxAddr = append(callbacksOnNewTxAddr, publicServer.OnNewTxAddr) 290 } 291 292 if *synchronize { 293 internalState.SyncMode = true 294 internalState.InitialSync = true 295 if err := syncWorker.ResyncIndex(nil, true); err != nil { 296 glog.Error("resyncIndex ", err) 297 return 298 } 299 var mempoolCount int 300 if mempoolCount, err = chain.ResyncMempool(nil); err != nil { 301 glog.Error("resyncMempool ", err) 302 return 303 } 304 internalState.FinishedMempoolSync(mempoolCount) 305 go syncIndexLoop() 306 go syncMempoolLoop() 307 internalState.InitialSync = false 308 } 309 go storeInternalStateLoop() 310 311 if *publicBinding != "" { 312 // start full public interface 313 publicServer.ConnectFullPublicInterface() 314 } 315 316 if *blockFrom >= 0 { 317 if *blockUntil < 0 { 318 *blockUntil = *blockFrom 319 } 320 height := uint32(*blockFrom) 321 until := uint32(*blockUntil) 322 address := *queryAddress 323 324 if address != "" { 325 if err = index.GetTransactions(address, height, until, printResult); err != nil { 326 glog.Error("GetTransactions ", err) 327 return 328 } 329 } else if !*synchronize { 330 if err = syncWorker.ConnectBlocksParallel(height, until); err != nil { 331 glog.Error("connectBlocksParallel ", err) 332 return 333 } 334 } 335 } 336 337 if internalServer != nil || publicServer != nil || chain != nil { 338 waitForSignalAndShutdown(internalServer, publicServer, chain, 10*time.Second) 339 } 340 341 if *synchronize { 342 close(chanSyncIndex) 343 close(chanSyncMempool) 344 close(chanStoreInternalState) 345 <-chanSyncIndexDone 346 <-chanSyncMempoolDone 347 <-chanStoreInternalStateDone 348 } 349 } 350 351 func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { 352 api, err := api.NewWorker(db, chain, txCache, is) 353 if err != nil { 354 return err 355 } 356 si, err := api.GetSystemInfo(false) 357 if err != nil { 358 return err 359 } 360 metrics.BlockbookAppInfo.Reset() 361 metrics.BlockbookAppInfo.With(common.Labels{ 362 "blockbook_version": si.Blockbook.Version, 363 "blockbook_commit": si.Blockbook.GitCommit, 364 "blockbook_buildtime": si.Blockbook.BuildTime, 365 "backend_version": si.Backend.Version, 366 "backend_subversion": si.Backend.Subversion, 367 "backend_protocol_version": si.Backend.ProtocolVersion}).Set(float64(0)) 368 return nil 369 } 370 371 func newInternalState(coin, coinShortcut, coinLabel string, d *db.RocksDB) (*common.InternalState, error) { 372 is, err := d.LoadInternalState(coin) 373 if err != nil { 374 return nil, err 375 } 376 is.CoinShortcut = coinShortcut 377 if coinLabel == "" { 378 coinLabel = coin 379 } 380 is.CoinLabel = coinLabel 381 name, err := os.Hostname() 382 if err != nil { 383 glog.Error("get hostname ", err) 384 } else { 385 if i := strings.IndexByte(name, '.'); i > 0 { 386 name = name[:i] 387 } 388 is.Host = name 389 } 390 return is, nil 391 } 392 393 func tickAndDebounce(tickTime time.Duration, debounceTime time.Duration, input chan struct{}, f func()) { 394 timer := time.NewTimer(tickTime) 395 var firstDebounce time.Time 396 Loop: 397 for { 398 select { 399 case _, ok := <-input: 400 if !timer.Stop() { 401 <-timer.C 402 } 403 // exit loop on closed input channel 404 if !ok { 405 break Loop 406 } 407 if firstDebounce.IsZero() { 408 firstDebounce = time.Now() 409 } 410 // debounce for up to debounceTime period 411 // afterwards execute immediately 412 if firstDebounce.Add(debounceTime).After(time.Now()) { 413 timer.Reset(debounceTime) 414 } else { 415 timer.Reset(0) 416 } 417 case <-timer.C: 418 // do the action, if not in shutdown, then start the loop again 419 if atomic.LoadInt32(&inShutdown) == 0 { 420 f() 421 } 422 timer.Reset(tickTime) 423 firstDebounce = time.Time{} 424 } 425 } 426 } 427 428 func syncIndexLoop() { 429 defer close(chanSyncIndexDone) 430 glog.Info("syncIndexLoop starting") 431 // resync index about every 15 minutes if there are no chanSyncIndex requests, with debounce 1 second 432 tickAndDebounce(time.Duration(*resyncIndexPeriodMs)*time.Millisecond, debounceResyncIndexMs*time.Millisecond, chanSyncIndex, func() { 433 if err := syncWorker.ResyncIndex(onNewBlockHash, false); err != nil { 434 glog.Error("syncIndexLoop ", errors.ErrorStack(err)) 435 } 436 }) 437 glog.Info("syncIndexLoop stopped") 438 } 439 440 func onNewBlockHash(hash string, height uint32) { 441 for _, c := range callbacksOnNewBlock { 442 c(hash, height) 443 } 444 } 445 446 func syncMempoolLoop() { 447 defer close(chanSyncMempoolDone) 448 glog.Info("syncMempoolLoop starting") 449 // resync mempool about every minute if there are no chanSyncMempool requests, with debounce 1 second 450 tickAndDebounce(time.Duration(*resyncMempoolPeriodMs)*time.Millisecond, debounceResyncMempoolMs*time.Millisecond, chanSyncMempool, func() { 451 internalState.StartedMempoolSync() 452 if count, err := chain.ResyncMempool(onNewTxAddr); err != nil { 453 glog.Error("syncMempoolLoop ", errors.ErrorStack(err)) 454 } else { 455 internalState.FinishedMempoolSync(count) 456 457 } 458 }) 459 glog.Info("syncMempoolLoop stopped") 460 } 461 462 func storeInternalStateLoop() { 463 stopCompute := make(chan os.Signal) 464 defer func() { 465 close(stopCompute) 466 close(chanStoreInternalStateDone) 467 }() 468 var computeRunning bool 469 lastCompute := time.Now() 470 // randomize the duration between ComputeInternalStateColumnStats to avoid peaks after reboot of machine with multiple blockbooks 471 computePeriod := 23*time.Hour + time.Duration(rand.Float64()*float64((4*time.Hour).Nanoseconds())) 472 lastAppInfo := time.Now() 473 logAppInfoPeriod := 15 * time.Minute 474 glog.Info("storeInternalStateLoop starting with db stats recompute period ", computePeriod) 475 tickAndDebounce(storeInternalStatePeriodMs*time.Millisecond, (storeInternalStatePeriodMs-1)*time.Millisecond, chanStoreInternalState, func() { 476 if !computeRunning && lastCompute.Add(computePeriod).Before(time.Now()) { 477 computeRunning = true 478 go func() { 479 err := index.ComputeInternalStateColumnStats(stopCompute) 480 if err != nil { 481 glog.Error("computeInternalStateColumnStats error: ", err) 482 } 483 lastCompute = time.Now() 484 computeRunning = false 485 }() 486 } 487 if err := index.StoreInternalState(internalState); err != nil { 488 glog.Error("storeInternalStateLoop ", errors.ErrorStack(err)) 489 } 490 if lastAppInfo.Add(logAppInfoPeriod).Before(time.Now()) { 491 glog.Info(index.GetMemoryStats()) 492 if err := blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { 493 glog.Error("blockbookAppInfoMetric ", err) 494 } 495 lastAppInfo = time.Now() 496 } 497 }) 498 glog.Info("storeInternalStateLoop stopped") 499 } 500 501 func onNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) { 502 for _, c := range callbacksOnNewTxAddr { 503 c(txid, desc, isOutput) 504 } 505 } 506 507 func pushSynchronizationHandler(nt bchain.NotificationType) { 508 if atomic.LoadInt32(&inShutdown) != 0 { 509 return 510 } 511 glog.V(1).Info("MQ: notification ", nt) 512 if nt == bchain.NotificationNewBlock { 513 chanSyncIndex <- struct{}{} 514 } else if nt == bchain.NotificationNewTx { 515 chanSyncMempool <- struct{}{} 516 } else { 517 glog.Error("MQ: unknown notification sent") 518 } 519 } 520 521 func waitForSignalAndShutdown(internal *server.InternalServer, public *server.PublicServer, chain bchain.BlockChain, timeout time.Duration) { 522 sig := <-chanOsSignal 523 atomic.StoreInt32(&inShutdown, 1) 524 glog.Infof("shutdown: %v", sig) 525 526 ctx, cancel := context.WithTimeout(context.Background(), timeout) 527 defer cancel() 528 529 if internal != nil { 530 if err := internal.Shutdown(ctx); err != nil { 531 glog.Error("internal server: shutdown error: ", err) 532 } 533 } 534 535 if public != nil { 536 if err := public.Shutdown(ctx); err != nil { 537 glog.Error("public server: shutdown error: ", err) 538 } 539 } 540 541 if chain != nil { 542 if err := chain.Shutdown(ctx); err != nil { 543 glog.Error("rpc: shutdown error: ", err) 544 } 545 } 546 } 547 548 func printResult(txid string, vout uint32, isOutput bool) error { 549 glog.Info(txid, vout, isOutput) 550 return nil 551 } 552 553 func normalizeName(s string) string { 554 s = strings.ToLower(s) 555 s = strings.Replace(s, " ", "-", -1) 556 return s 557 }