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