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  }