diff --git a/blockbook.go b/blockbook.go index a0893ec9..da8a067a 100644 --- a/blockbook.go +++ b/blockbook.go @@ -33,6 +33,10 @@ const debounceResyncMempoolMs = 1009 // store internal state about once every minute const storeInternalStatePeriodMs = 59699 +// exit codes from the main function +const exitCodeOK = 0 +const exitCodeFatal = 255 + var ( blockchain = flag.String("blockchaincfg", "", "path to blockchain RPC service configuration json file") @@ -100,6 +104,11 @@ func init() { } func main() { + os.Exit(mainWithExitCode()) +} + +// allow deferred functions to run even in case of fatal error +func mainWithExitCode() int { flag.Parse() defer glog.Flush() @@ -120,20 +129,20 @@ func main() { if *repair { if err := db.RepairRocksDB(*dbPath); err != nil { glog.Errorf("RepairRocksDB %s: %v", *dbPath, err) - return + return exitCodeFatal } - return + return exitCodeOK } if *blockchain == "" { glog.Error("Missing blockchaincfg configuration parameter") - return + return exitCodeFatal } coin, coinShortcut, coinLabel, err := coins.GetCoinNameFromConfig(*blockchain) if err != nil { glog.Error("config: ", err) - return + return exitCodeFatal } // gspt.SetProcTitle("blockbook-" + normalizeName(coin)) @@ -141,31 +150,31 @@ func main() { metrics, err = common.GetMetrics(coin) if err != nil { glog.Error("metrics: ", err) - return + return exitCodeFatal } - if chain, mempool, err = getBlockChainWithRetry(coin, *blockchain, pushSynchronizationHandler, metrics, 60); err != nil { + if chain, mempool, err = getBlockChainWithRetry(coin, *blockchain, pushSynchronizationHandler, metrics, 120); err != nil { glog.Error("rpc: ", err) - return + return exitCodeFatal } index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics) if err != nil { glog.Error("rocksDB: ", err) - return + return exitCodeFatal } defer index.Close() internalState, err = newInternalState(coin, coinShortcut, coinLabel, index) if err != nil { glog.Error("internalState: ", err) - return + return exitCodeFatal } index.SetInternalState(internalState) if internalState.DbState != common.DbStateClosed { if internalState.DbState == common.DbStateInconsistent { glog.Error("internalState: database is in inconsistent state and cannot be used") - return + return exitCodeFatal } glog.Warning("internalState: database was left in open state, possibly previous ungraceful shutdown") } @@ -175,15 +184,16 @@ func main() { err = index.ComputeInternalStateColumnStats(chanOsSignal) if err != nil { glog.Error("internalState: ", err) + return exitCodeFatal } glog.Info("DB size on disk: ", index.DatabaseSizeOnDisk(), ", DB size as computed: ", internalState.DBSizeTotal()) - return + return exitCodeOK } syncWorker, err = db.NewSyncWorker(index, chain, *syncWorkers, *syncChunk, *blockFrom, *dryRun, chanOsSignal, metrics, internalState) if err != nil { glog.Errorf("NewSyncWorker %v", err) - return + return exitCodeFatal } // set the DbState to open at this moment, after all important workers are initialized @@ -191,17 +201,20 @@ func main() { err = index.StoreInternalState(internalState) if err != nil { glog.Error("internalState: ", err) - return + return exitCodeFatal } if *rollbackHeight >= 0 { - performRollback() - return + err = performRollback() + if err != nil { + return exitCodeFatal + } + return exitCodeOK } if txCache, err = db.NewTxCache(index, chain, metrics, internalState, !*noTxCache); err != nil { glog.Error("txCache ", err) - return + return exitCodeFatal } // report BlockbookAppInfo metric, only log possible error @@ -214,7 +227,7 @@ func main() { internalServer, err = startInternalServer() if err != nil { glog.Error("internal server: ", err) - return + return exitCodeFatal } } @@ -223,7 +236,7 @@ func main() { publicServer, err = startPublicServer() if err != nil { glog.Error("public server: ", err) - return + return exitCodeFatal } } @@ -231,8 +244,11 @@ func main() { internalState.SyncMode = true internalState.InitialSync = true if err := syncWorker.ResyncIndex(nil, true); err != nil { - glog.Error("resyncIndex ", err) - return + if err != db.ErrSyncInterrupted { + glog.Error("resyncIndex ", err) + return exitCodeFatal + } + return exitCodeOK } // initialize mempool after the initial sync is complete var addrDescForOutpoint bchain.AddrDescForOutpointFunc @@ -242,12 +258,12 @@ func main() { err = chain.InitializeMempool(addrDescForOutpoint, onNewTxAddr) if err != nil { glog.Error("initializeMempool ", err) - return + return exitCodeFatal } var mempoolCount int if mempoolCount, err = mempool.Resync(); err != nil { glog.Error("resyncMempool ", err) - return + return exitCodeFatal } internalState.FinishedMempoolSync(mempoolCount) go syncIndexLoop() @@ -272,8 +288,11 @@ func main() { if !*synchronize { if err = syncWorker.ConnectBlocksParallel(height, until); err != nil { - glog.Error("connectBlocksParallel ", err) - return + if err != db.ErrSyncInterrupted { + glog.Error("connectBlocksParallel ", err) + return exitCodeFatal + } + return exitCodeOK } } } @@ -290,6 +309,7 @@ func main() { <-chanSyncMempoolDone <-chanStoreInternalStateDone } + return exitCodeOK } func getBlockChainWithRetry(coin string, configfile string, pushHandler func(bchain.NotificationType), metrics *common.Metrics, seconds int) (bchain.BlockChain, bchain.Mempool, error) { @@ -355,11 +375,11 @@ func startPublicServer() (*server.PublicServer, error) { return publicServer, err } -func performRollback() { +func performRollback() error { bestHeight, bestHash, err := index.GetBestBlock() if err != nil { glog.Error("rollbackHeight: ", err) - return + return err } if uint32(*rollbackHeight) > bestHeight { glog.Infof("nothing to rollback, rollbackHeight %d, bestHeight: %d", *rollbackHeight, bestHeight) @@ -369,16 +389,17 @@ func performRollback() { hash, err := index.GetBlockHash(height) if err != nil { glog.Error("rollbackHeight: ", err) - return + return err } hashes = append(hashes, hash) } err = syncWorker.DisconnectBlocks(uint32(*rollbackHeight), bestHeight, hashes) if err != nil { glog.Error("rollbackHeight: ", err) - return + return err } } + return nil } func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is *common.InternalState, metrics *common.Metrics) error { diff --git a/db/sync.go b/db/sync.go index e81cbb2c..7d2a2d9f 100644 --- a/db/sync.go +++ b/db/sync.go @@ -45,6 +45,9 @@ func NewSyncWorker(db *RocksDB, chain bchain.BlockChain, syncWorkers, syncChunk var errSynced = errors.New("synced") +// ErrSyncInterrupted is returned when synchronization is interrupted by OS signal +var ErrSyncInterrupted = errors.New("ErrSyncInterrupted") + // ResyncIndex synchronizes index to the top of the blockchain // onNewBlock is called when new block is connected, but not in initial parallel sync func (w *SyncWorker) ResyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync bool) error { @@ -202,7 +205,8 @@ func (w *SyncWorker) connectBlocks(onNewBlock bchain.OnNewBlockFunc, initialSync for { select { case <-w.chanOsSignal: - return errors.Errorf("connectBlocks interrupted at height %d", lastRes.block.Height) + glog.Info("connectBlocks interrupted at height ", lastRes.block.Height) + return ErrSyncInterrupted case res := <-bch: if res == empty { break ConnectLoop @@ -325,7 +329,8 @@ ConnectLoop: for h := lower; h <= higher; { select { case <-w.chanOsSignal: - err = errors.Errorf("connectBlocksParallel interrupted at height %d", h) + glog.Info("connectBlocksParallel interrupted at height ", h) + err = ErrSyncInterrupted // signal all workers to terminate their loops (error loops are interrupted below) close(terminating) break ConnectLoop diff --git a/tests/sync/connectblocks.go b/tests/sync/connectblocks.go index a3b9da9f..0dffdf20 100644 --- a/tests/sync/connectblocks.go +++ b/tests/sync/connectblocks.go @@ -25,10 +25,8 @@ func testConnectBlocks(t *testing.T, h *TestHandler) { close(ch) } }, true) - if err != nil { - if !strings.HasPrefix(err.Error(), "connectBlocks interrupted at height") { - t.Fatal(err) - } + if err != nil && err != db.ErrSyncInterrupted { + t.Fatal(err) } height, _, err := d.GetBestBlock()