github.com/aychain/blockbook@v0.1.1-0.20181121092459-6d1fc7e07c5b/blockbook.go (about)

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