github.com/almightyhelp/blockbook@v0.4.2-0.20210524123154-8697b01c4af9/blockbook.go (about)

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