github.com/trezor/blockbook@v0.4.1-0.20240328132726-e9a08582ee2c/blockbook.go (about)

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