diff --git a/api/apiroutes.go b/api/apiroutes.go index f32d3146f..8f224bd32 100644 --- a/api/apiroutes.go +++ b/api/apiroutes.go @@ -94,7 +94,7 @@ type DataSourceAux interface { txnType dbtypes.AddrTxnType) (*apitypes.Address, error) AddressTotals(address string) (*apitypes.AddressTotals, error) VotesInBlock(hash string) (int16, error) - GetTxHistoryData(address string, addrChart dbtypes.HistoryChart, + TxHistoryData(address string, addrChart dbtypes.HistoryChart, chartGroupings dbtypes.TimeBasedGrouping) (*dbtypes.ChartsData, error) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ( []*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) @@ -118,8 +118,8 @@ func NewContext(client *rpcclient.Client, params *chaincfg.Params, dataSource Da conns, _ := client.GetConnectionCount() nodeHeight, _ := client.GetBlockCount() - // explorerDataSource is an interface that could have a value of pointer - // type, and if either is nil this means lite mode. + // auxDataSource is an interface that could have a value of pointer type, + // and if either is nil this means lite mode. liteMode := auxDataSource == nil || reflect.ValueOf(auxDataSource).IsNil() return &appContext{ @@ -428,6 +428,9 @@ func (c *appContext) setOutputSpends(txid string, vouts []apitypes.Vout) error { // For each output of this transaction, look up any spending transactions, // and the index of the spending transaction input. spendHashes, spendVinInds, voutInds, err := c.AuxDataSource.SpendingTransactions(txid) + if dbtypes.IsTimeoutErr(err) { + return fmt.Errorf("SpendingTransactions: %v", err) + } if err != nil && err != sql.ErrNoRows { return fmt.Errorf("unable to get spending transaction info for outputs of %s", txid) } @@ -835,9 +838,14 @@ func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request) // needed by this request. interval := dbtypes.TimeGroupingFromStr(tp) barCharts, _, height, err := c.AuxDataSource.TicketPoolVisualization(interval) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("TicketPoolVisualization: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { apiLog.Errorf("Unable to get ticket pool by date: %v", err) - http.Error(w, http.StatusText(422), 422) + http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity) return } @@ -887,6 +895,11 @@ func (c *appContext) blockSubsidies(w http.ResponseWriter, r *http.Request) { if hash != "" { var err error numVotes, err = c.AuxDataSource.VotesInBlock(hash) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("VotesInBlock: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { http.NotFound(w, r) return @@ -1215,6 +1228,11 @@ func (c *appContext) addressTotals(w http.ResponseWriter, r *http.Request) { } totals, err := c.AuxDataSource.AddressTotals(address) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("AddressTotals: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { log.Warnf("failed to get address totals (%s): %v", address, err) http.Error(w, http.StatusText(422), 422) @@ -1237,8 +1255,13 @@ func (c *appContext) getAddressTxTypesData(w http.ResponseWriter, r *http.Reques return } - data, err := c.AuxDataSource.GetTxHistoryData(address, dbtypes.TxsType, + data, err := c.AuxDataSource.TxHistoryData(address, dbtypes.TxsType, dbtypes.TimeGroupingFromStr(chartGrouping)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("TxHistoryData: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { log.Warnf("failed to get address (%s) history by tx type : %v", address, err) http.Error(w, http.StatusText(422), 422) @@ -1261,8 +1284,13 @@ func (c *appContext) getAddressTxAmountFlowData(w http.ResponseWriter, r *http.R return } - data, err := c.AuxDataSource.GetTxHistoryData(address, dbtypes.AmountFlow, + data, err := c.AuxDataSource.TxHistoryData(address, dbtypes.AmountFlow, dbtypes.TimeGroupingFromStr(chartGrouping)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("TxHistoryData: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { log.Warnf("failed to get address (%s) history by amount flow: %v", address, err) http.Error(w, http.StatusText(422), 422) @@ -1285,8 +1313,13 @@ func (c *appContext) getAddressTxUnspentAmountData(w http.ResponseWriter, r *htt return } - data, err := c.AuxDataSource.GetTxHistoryData(address, dbtypes.TotalUnspent, + data, err := c.AuxDataSource.TxHistoryData(address, dbtypes.TotalUnspent, dbtypes.TimeGroupingFromStr(chartGrouping)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("TxHistoryData: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { log.Warnf("failed to get address (%s) history by unspent amount flow: %v", address, err) http.Error(w, http.StatusText(422), 422) @@ -1353,8 +1386,12 @@ func (c *appContext) getAddressTransactions(w http.ResponseWriter, r *http.Reque txs = c.BlockData.GetAddressTransactionsWithSkip(address, int(count), int(skip)) } else { txs, err = c.AuxDataSource.AddressTransactionDetails(address, count, skip, dbtypes.AddrTxnAll) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("AddressTransactionDetails: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } } - if txs == nil || err != nil { http.Error(w, http.StatusText(422), 422) return diff --git a/api/insight/apiroutes.go b/api/insight/apiroutes.go index 971c85413..6acb5bbc2 100644 --- a/api/insight/apiroutes.go +++ b/api/insight/apiroutes.go @@ -163,6 +163,11 @@ func (c *insightApiContext) getBlockSummary(w http.ResponseWriter, r *http.Reque } var err error hash, err = c.BlockData.ChainDB.GetBlockHash(int64(idx)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("GetBlockHash: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { writeInsightError(w, "Unable to get block hash from index") return @@ -196,6 +201,11 @@ func (c *insightApiContext) getBlockHash(w http.ResponseWriter, r *http.Request) return } hash, err := c.BlockData.ChainDB.GetBlockHash(int64(idx)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("GetBlockHash: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil || hash == "" { writeInsightNotFound(w, "Not found") return @@ -229,6 +239,11 @@ func (c *insightApiContext) getRawBlock(w http.ResponseWriter, r *http.Request) } var err error hash, err = c.BlockData.ChainDB.GetBlockHash(int64(idx)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("GetBlockHash: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { writeInsightError(w, "Unable to get block hash from index") return @@ -305,12 +320,21 @@ func (c *insightApiContext) getAddressesTxnOutput(w http.ResponseWriter, r *http txnOutputs := make([]apitypes.AddressTxnOutput, 0) for _, address := range addresses { - - confirmedTxnOutputs := c.BlockData.ChainDB.GetAddressUTXO(address) + confirmedTxnOutputs, err := c.BlockData.ChainDB.AddressUTXO(address) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("AddressUTXO: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + apiLog.Errorf("Error getting UTXOs: %v", err) + continue + } addressOuts, _, err := c.MemPool.UnconfirmedTxnsForAddress(address) if err != nil { - apiLog.Errorf("Error in getting unconfirmed transactions") + apiLog.Errorf("Error getting unconfirmed transactions: %v", err) + continue } if addressOuts != nil { @@ -445,13 +469,25 @@ func (c *insightApiContext) getTransactions(w http.ResponseWriter, r *http.Reque return } addresses := []string{address} - rawTxs, recentTxs := c.BlockData.ChainDB.InsightPgGetAddressTransactions(addresses, int64(c.Status.Height-2)) + rawTxs, recentTxs, err := + c.BlockData.ChainDB.InsightAddressTransactions(addresses, int64(c.Status.Height-2)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("InsightAddressTransactions: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + writeInsightError(w, + fmt.Sprintf("Error retrieving transactions for addresss %s (%v)", + addresses, err)) + return + } addressOuts, _, err := c.MemPool.UnconfirmedTxnsForAddress(address) UnconfirmedTxs := []string{} if err != nil { - writeInsightError(w, fmt.Sprintf("Error gathering mempool transactions (%s)", err)) + writeInsightError(w, fmt.Sprintf("Error gathering mempool transactions (%v)", err)) return } @@ -536,7 +572,19 @@ func (c *insightApiContext) getAddressesTxn(w http.ResponseWriter, r *http.Reque addressOutput := new(apitypes.InsightMultiAddrsTxOutput) UnconfirmedTxs := []string{} - rawTxs, recentTxs := c.BlockData.ChainDB.InsightPgGetAddressTransactions(addresses, int64(c.Status.Height-2)) + rawTxs, recentTxs, err := + c.BlockData.ChainDB.InsightAddressTransactions(addresses, int64(c.Status.Height-2)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("InsightAddressTransactions: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + writeInsightError(w, + fmt.Sprintf("Error retrieving transactions for addresss %s (%s)", + addresses, err)) + return + } // Confirm all addresses are valid and pull unconfirmed transactions for all addresses for _, addr := range addresses { @@ -639,8 +687,14 @@ func (c *insightApiContext) getAddressBalance(w http.ResponseWriter, r *http.Req return } - addressInfo := c.BlockData.ChainDB.GetAddressBalance(address, 20, 0) - if addressInfo == nil { + addressInfo, err := c.BlockData.ChainDB.AddressBalance(address, 20, 0) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("AddressBalance: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil || addressInfo == nil { + apiLog.Warnf("AddressBalance: %v", err) http.Error(w, http.StatusText(422), 422) return } @@ -811,11 +865,20 @@ func (c *insightApiContext) getBlockSummaryByTime(w http.ResponseWriter, r *http summaryOutput.Pagination.CurrentTs = maxTime summaryOutput.Pagination.MoreTs = maxTime - blockSummary := c.BlockData.ChainDB.GetBlockSummaryTimeRange(minTime, maxTime, 0) + blockSummary, err := c.BlockData.ChainDB.BlockSummaryTimeRange(minTime, maxTime, 0) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("BlockSummaryTimeRange: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + writeInsightError(w, fmt.Sprintf("Unable to retrieve block summaries: %v", err)) + return + } outputBlockSummary := []dbtypes.BlockDataBasic{} - // Generate the pagenation parameters more and moreTs and limit the result + // Generate the pagination parameters More and MoreTs and limit the result. if limit > 0 { for i, block := range blockSummary { if i >= limit { @@ -861,8 +924,15 @@ func (c *insightApiContext) getAddressInfo(w http.ResponseWriter, r *http.Reques // Get Confirmed Balances var unconfirmedBalanceSat int64 - _, _, totalSpent, totalUnspent, _, err := c.BlockData.ChainDB.RetrieveAddressSpentUnspent(address) + _, _, totalSpent, totalUnspent, _, err := c.BlockData.ChainDB.AddressSpentUnspent(address) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("AddressSpentUnspent: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } if err != nil { + apiLog.Errorf("AddressSpentUnspent: %v", err) + http.Error(w, "Unexpected error retrieving address info.", http.StatusInternalServerError) return } @@ -882,11 +952,24 @@ func (c *insightApiContext) getAddressInfo(w http.ResponseWriter, r *http.Reques addresses := []string{address} - // Get Confirmed Transactions - rawTxs, recentTxs := c.BlockData.ChainDB.InsightPgGetAddressTransactions(addresses, int64(c.Status.Height-2)) + // Get confirmed transactions. + rawTxs, recentTxs, err := + c.BlockData.ChainDB.InsightAddressTransactions(addresses, int64(c.Status.Height-2)) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("InsightAddressTransactions: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + apiLog.Errorf("Error retrieving transactions for addresss %s: %v", + addresses, err) + http.Error(w, "Error retrieving transactions for that addresss.", + http.StatusInternalServerError) + return + } confirmedTxCount := len(rawTxs) - // Get Unconfirmed Transactions + // Get unconfirmed transactions. unconfirmedTxs := []string{} addressOuts, _, err := c.MemPool.UnconfirmedTxnsForAddress(address) if err != nil { @@ -895,7 +978,7 @@ func (c *insightApiContext) getAddressInfo(w http.ResponseWriter, r *http.Reques if addressOuts != nil { FUNDING_TX_DUPLICATE_CHECK: for _, f := range addressOuts.Outpoints { - // Confirm its not already in our recent transactions + // Confirm it's not already in our recent transactions. for _, v := range recentTxs { if v == f.Hash.String() { continue FUNDING_TX_DUPLICATE_CHECK diff --git a/api/insight/converter.go b/api/insight/converter.go index 11cc6bc72..51bf5b167 100644 --- a/api/insight/converter.go +++ b/api/insight/converter.go @@ -16,12 +16,12 @@ func (c *insightApiContext) TxConverter(txs []*dcrjson.TxRawResult) ([]apitypes. return c.DcrToInsightTxns(txs, false, false, false) } -// DcrToInsightTxns takes struct with filter params -func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, - noAsm, noScriptSig, noSpent bool) ([]apitypes.InsightTx, error) { +// DcrToInsightTxns converts a dcrjson TxRawResult to a InsightTx. The asm, +// scriptSig, and spending status may be skipped by setting the appropriate +// input arguments. +func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, noAsm, noScriptSig, noSpent bool) ([]apitypes.InsightTx, error) { var newTxs []apitypes.InsightTx for _, tx := range txs { - // Build new InsightTx txNew := apitypes.InsightTx{ Txid: tx.Txid, @@ -36,10 +36,8 @@ func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, } // Vins fill - var vInSum, vOutSum float64 - + var vInSum float64 for vinID, vin := range tx.Vin { - InsightVin := &apitypes.InsightVin{ Txid: vin.Txid, Vout: vin.Vout, @@ -60,8 +58,9 @@ func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, } } - // Note, this only gathers information from the database which does not include mempool transactions - _, addresses, value, err := c.BlockData.ChainDB.RetrieveAddressIDsByOutpoint(vin.Txid, vin.Vout) + // Note: this only gathers information from the database, which does + // not include mempool transactions. + _, addresses, value, err := c.BlockData.ChainDB.AddressIDsByOutpoint(vin.Txid, vin.Vout) if err == nil { if len(addresses) > 0 { // Update Vin due to DCRD AMOUNTIN - START @@ -82,6 +81,7 @@ func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, } // Vout fill + var vOutSum float64 for _, v := range tx.Vout { InsightVout := &apitypes.InsightVout{ Value: v.Value, @@ -109,7 +109,7 @@ func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, dcramt, _ = dcrutil.NewAmount(txNew.ValueIn - txNew.ValueOut) txNew.Fees = dcramt.ToCoin() - // Return true if coinbase value is not empty, return 0 at some fields + // Return true if coinbase value is not empty, return 0 at some fields. if txNew.Vins != nil && txNew.Vins[0].CoinBase != "" { txNew.IsCoinBase = true txNew.ValueIn = 0 @@ -121,9 +121,13 @@ func (c *insightApiContext) DcrToInsightTxns(txs []*dcrjson.TxRawResult, } if !noSpent { - // populate the spending status of all vouts - // Note, this only gathers information from the database which does not include mempool transactions - addrFull := c.BlockData.ChainDB.GetSpendDetailsByFundingHash(txNew.Txid) + // Populate the spending status of all vouts. Note: this only + // gathers information from the database, which does not include + // mempool transactions. + addrFull, err := c.BlockData.ChainDB.SpendDetailsForFundingTx(txNew.Txid) + if err != nil { + return nil, err + } for _, dbaddr := range addrFull { txNew.Vouts[dbaddr.FundingTxVoutIndex].SpentIndex = dbaddr.SpendingTxVinIndex txNew.Vouts[dbaddr.FundingTxVoutIndex].SpentTxID = dbaddr.SpendingTxHash diff --git a/api/types/insightapitypes.go b/api/types/insightapitypes.go index 83e9589f0..ca7852129 100644 --- a/api/types/insightapitypes.go +++ b/api/types/insightapitypes.go @@ -1,3 +1,4 @@ +// Copyright (c) 2018, The Decred developers // Copyright (c) 2017, Jonathan Chappelow // See LICENSE for details. @@ -9,8 +10,7 @@ import ( "github.com/decred/dcrdata/v3/db/dbtypes" ) -// InsightAddress models an address transactions -// +// InsightAddress models an address' transactions. type InsightAddress struct { Address string `json:"address,omitempty"` From int `json:"from"` @@ -18,8 +18,7 @@ type InsightAddress struct { Transactions []InsightTx `json:"items,omitempty"` } -// InsightAddressInfo models basic information -// about an address +// InsightAddressInfo models basic information about an address. type InsightAddressInfo struct { Address string `json:"addrStr,omitempty"` Balance float64 `json:"balance"` @@ -35,13 +34,12 @@ type InsightAddressInfo struct { TransactionsID []string `json:"transactions,omitempty"` } -// InsightRawTx contains the raw transaction string -// of a transaction +// InsightRawTx contains the raw transaction string of a transaction. type InsightRawTx struct { Rawtx string `json:"rawtx"` } -// InsightMultiAddrsTx models multi address post data structure +// InsightMultiAddrsTx models multi-address post data structure. type InsightMultiAddrsTx struct { Addresses string `json:"addrs"` From json.Number `json:"from,Number,omitempty"` @@ -58,20 +56,19 @@ type InsightMultiAddrsTxOutput struct { Items []InsightTx `json:"items"` } -// InsightAddr models the multi address post data structure +// InsightAddr models the multi-address post data structure. type InsightAddr struct { Addrs string `json:"addrs"` } -// InsightPagination models basic pagination output -// for a result +// InsightPagination models basic pagination output for a result. type InsightPagination struct { Next string `json:"next,omitempty"` Prev string `json:"prev,omitempty"` IsToday string `json:"isToday,omitempty"` } -// AddressTxnOutput models an address transaction outputs +// AddressTxnOutput models an address transaction outputs. type AddressTxnOutput struct { Address string `json:"address"` TxnID string `json:"txid"` @@ -86,8 +83,7 @@ type AddressTxnOutput struct { Confirmations int64 `json:"confirmations"` } -// SpendByFundingHash models a return from -// GetSpendDetailsByFundingHash +// SpendByFundingHash models a return from SpendDetailsForFundingTx. type SpendByFundingHash struct { FundingTxVoutIndex uint32 SpendingTxVinIndex interface{} diff --git a/config.go b/config.go index 406720b12..0a13076fc 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,7 @@ import ( "runtime" "sort" "strings" + "time" flags "github.com/btcsuite/go-flags" "github.com/caarlos0/env" @@ -59,10 +60,11 @@ var ( defaultDBFileName = "dcrdata.sqlt.db" defaultAgendDBFileName = "agendas.db" - defaultPGHost = "127.0.0.1:5432" - defaultPGUser = "dcrdata" - defaultPGPass = "" - defaultPGDBName = "dcrdata" + defaultPGHost = "127.0.0.1:5432" + defaultPGUser = "dcrdata" + defaultPGPass = "" + defaultPGDBName = "dcrdata" + defaultPGQueryTimeout time.Duration = time.Hour ) type config struct { @@ -98,14 +100,16 @@ type config struct { DBFileName string `long:"dbfile" description:"SQLite DB file name (default is dcrdata.sqlt.db)." env:"DCRDATA_SQLITE_DB_FILE_NAME"` AgendaDBFileName string `long:"agendadbfile" description:"Agenda DB file name (default is agendas.db)." env:"DCRDATA_AGENDA_DB_FILE_NAME"` - FullMode bool `long:"pg" description:"Run in \"Full Mode\" mode, enables postgresql support" env:"DCRDATA_ENABLE_FULL_MODE"` - PGDBName string `long:"pgdbname" description:"PostgreSQL DB name." env:"DCRDATA_PG_DB_NAME"` - PGUser string `long:"pguser" description:"PostgreSQL DB user." env:"DCRDATA_POSTGRES_USER"` - PGPass string `long:"pgpass" description:"PostgreSQL DB password." env:"DCRDATA_POSTGRES_PASS"` - PGHost string `long:"pghost" description:"PostgreSQL server host:port or UNIX socket (e.g. /run/postgresql)." env:"DCRDATA_POSTGRES_HOST_URL"` - NoDevPrefetch bool `long:"no-dev-prefetch" description:"Disable automatic dev fund balance query on new blocks. When true, the query will still be run on demand, but not automatically after new blocks are connected." env:"DCRDATA_DISABLE_DEV_PREFETCH"` - SyncAndQuit bool `long:"sync-and-quit" description:"Sync to the best block and exit. Do not start the explorer or API." env:"DCRDATA_ENABLE_SYNC_N_QUIT"` - ImportSideChains bool `long:"import-side-chains" description:"(experimental) Enable startup import of side chains retrieved from dcrd via getchaintips." env:"DCRDATA_IMPORT_SIDE_CHAINS"` + FullMode bool `long:"pg" description:"Run in \"Full Mode\" mode, enables postgresql support" env:"DCRDATA_ENABLE_FULL_MODE"` + PGDBName string `long:"pgdbname" description:"PostgreSQL DB name." env:"DCRDATA_PG_DB_NAME"` + PGUser string `long:"pguser" description:"PostgreSQL DB user." env:"DCRDATA_POSTGRES_USER"` + PGPass string `long:"pgpass" description:"PostgreSQL DB password." env:"DCRDATA_POSTGRES_PASS"` + PGHost string `long:"pghost" description:"PostgreSQL server host:port or UNIX socket (e.g. /run/postgresql)." env:"DCRDATA_POSTGRES_HOST_URL"` + PGQueryTimeout time.Duration `short:"T" long:"pgtimeout" description:"Timeout (a time.Duration string) for most PostgreSQL queries used for user initiated queries."` + + NoDevPrefetch bool `long:"no-dev-prefetch" description:"Disable automatic dev fund balance query on new blocks. When true, the query will still be run on demand, but not automatically after new blocks are connected." env:"DCRDATA_DISABLE_DEV_PREFETCH"` + SyncAndQuit bool `long:"sync-and-quit" description:"Sync to the best block and exit. Do not start the explorer or API." env:"DCRDATA_ENABLE_SYNC_N_QUIT"` + ImportSideChains bool `long:"import-side-chains" description:"(experimental) Enable startup import of side chains retrieved from dcrd via getchaintips." env:"DCRDATA_IMPORT_SIDE_CHAINS"` SyncStatusLimit int64 `long:"sync-status-limit" description:"Sets the number of blocks behind the current best height past which only the syncing status page can be served on the running web server. Value should be greater than 2 but less than 5000."` @@ -147,6 +151,7 @@ var ( PGUser: defaultPGUser, PGPass: defaultPGPass, PGHost: defaultPGHost, + PGQueryTimeout: defaultPGQueryTimeout, } ) @@ -521,6 +526,12 @@ func loadConfig() (*config, error) { cfg.DebugLevel = "error" } + // Validate DB timeout. Zero or negative should be set to the large default + // timeout to effectively disable timeouts. + if cfg.PGQueryTimeout <= 0 { + cfg.PGQueryTimeout = defaultPGQueryTimeout + } + // Parse, validate, and set debug log level(s). if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil { err = fmt.Errorf("%s: %v", funcName, err.Error()) diff --git a/config_test.go b/config_test.go index fef33f519..644de5323 100644 --- a/config_test.go +++ b/config_test.go @@ -29,8 +29,10 @@ func TestMain(m *testing.M) { flag.Parse() os.Args = os.Args[:1] // Run the tests now that the testing package flags have been parsed. - m.Run() + retCode := m.Run() os.Unsetenv("DCRDATA_CONFIG_FILE") + + os.Exit(retCode) } // disableConfigFileEnv checks if the DCRDATA_CONFIG_FILE environment variable diff --git a/db/dbtypes/types.go b/db/dbtypes/types.go index 560d7e689..aa1dc4600 100644 --- a/db/dbtypes/types.go +++ b/db/dbtypes/types.go @@ -4,6 +4,7 @@ package dbtypes import ( + "context" "database/sql/driver" "encoding/json" "fmt" @@ -14,6 +15,29 @@ import ( "github.com/decred/dcrdata/v3/db/dbtypes/internal" ) +var ( + // PGCancelError is the error string PostgreSQL returns when a query fails + // to complete due to user requested cancellation. + PGCancelError = "pq: canceling statement due to user request" + CtxDeadlineExceeded = context.DeadlineExceeded.Error() + TimeoutPrefix = "TIMEOUT of PostgreSQL query" +) + +// IsTimeout checks if the message is prefixed with the expected DB timeout +// message prefix. +func IsTimeout(msg string) bool { + // Contains is used instead of HasPrefix since error messages are often + // supplemented with additional information. + return strings.Contains(msg, TimeoutPrefix) || + strings.Contains(msg, CtxDeadlineExceeded) +} + +// IsTimeout checks if error's message is prefixed with the expected DB timeout +// message prefix. +func IsTimeoutErr(err error) bool { + return err != nil && IsTimeout(err.Error()) +} + // TimeDef is time.Time wrapper that formats time by default as a string without // a timezone. The time Stringer interface formats the time into a string // with a timezone. @@ -112,6 +136,14 @@ const ( UnknownGrouping ) +// TimeIntervals is a slice of distinct time intervals used for grouping data. +var TimeIntervals = []TimeBasedGrouping{ + YearGrouping, + MonthGrouping, + WeekGrouping, + DayGrouping, +} + const ( // InitialDBLoad is a sync where data is first loaded from the chain db into // the respective dbs currently supported. Runs on both liteMode and fullMode. @@ -317,7 +349,10 @@ func (p *VinTxPropertyARRAY) Scan(src interface{}) error { return fmt.Errorf("type assertion .(map[string]interface) failed") } b, _ := json.Marshal(VinTxPropertyMapIface) - json.Unmarshal(b, &ba[ii]) + err := json.Unmarshal(b, &ba[ii]) + if err != nil { + return err + } } *p = ba @@ -436,14 +471,14 @@ type AddressRow struct { } // AddressMetrics defines address metrics needed to make decisions by which -// grouping buttons on the address history page charts should be disabled -// or enabled by default. +// grouping buttons on the address history page charts should be disabled or +// enabled by default. type AddressMetrics struct { OldestBlockTime TimeDef - YearTxsCount int64 // Years txs grouping - MonthTxsCount int64 // Months txs grouping - WeekTxsCount int64 // Weeks txs grouping - DayTxsCount int64 // Days txs grouping + YearTxsCount int64 // number of year intervals with transactions + MonthTxsCount int64 // number of year month with transactions + WeekTxsCount int64 // number of year week with transactions + DayTxsCount int64 // number of year day with transactions } // ChartsData defines the fields that store the values needed to plot the charts diff --git a/db/dcrpg/insightapi.go b/db/dcrpg/insightapi.go index 7624b7792..09dbdb2cf 100644 --- a/db/dcrpg/insightapi.go +++ b/db/dcrpg/insightapi.go @@ -1,9 +1,12 @@ +// Copyright (c) 2018, The Decred developers // Copyright (c) 2017, The dcrdata developers // See LICENSE for details. package dcrpg import ( + "context" + "github.com/decred/dcrd/dcrjson" "github.com/decred/dcrd/dcrutil" apitypes "github.com/decred/dcrdata/v3/api/types" @@ -26,17 +29,25 @@ func (pgb *ChainDBRPC) GetRawTransaction(txid string) (*dcrjson.TxRawResult, err // GetBlockHeight returns the height of the block with the specified hash. func (pgb *ChainDB) GetBlockHeight(hash string) (int64, error) { - height, err := RetrieveBlockHeight(pgb.db, hash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + height, err := RetrieveBlockHeight(ctx, pgb.db, hash) if err != nil { log.Errorf("Unable to get block height for hash %s: %v", hash, err) - return -1, err + return -1, pgb.replaceCancelError(err) } return height, nil } // GetHeight returns the current best block height. func (pgb *ChainDB) GetHeight() int { - height, _, _, _ := RetrieveBestBlockHeight(pgb.db) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + height, _, _, err := RetrieveBestBlockHeight(ctx, pgb.db) + if err != nil { + // TODO: return err + log.Errorf("GetHeight: %v", pgb.replaceCancelError(err)) + } return int(height) } @@ -56,32 +67,38 @@ func (pgb *ChainDBRPC) SendRawTransaction(txhex string) (string, error) { return hash.String(), err } -// InsightPgGetAddressTransactions performs a db query to pull all txids for the +// InsightAddressTransactions performs a db query to pull all txids for the // specified addresses ordered desc by time. -func (pgb *ChainDB) InsightPgGetAddressTransactions(addr []string, - recentBlockHeight int64) ([]string, []string) { - return RetrieveAddressTxnsOrdered(pgb.db, addr, recentBlockHeight) +func (pgb *ChainDB) InsightAddressTransactions(addr []string, recentBlockHeight int64) ([]string, []string, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + txs, recentTxs, err := RetrieveAddressTxnsOrdered(ctx, pgb.db, addr, recentBlockHeight) + return txs, recentTxs, pgb.replaceCancelError(err) } -// RetrieveAddressSpentUnspent retrieves balance information for a specific -// address. -func (pgb *ChainDB) RetrieveAddressSpentUnspent(address string) (int64, int64, int64, int64, int64, error) { - return RetrieveAddressSpentUnspent(pgb.db, address) +// AddressSpentUnspent retrieves balance information for a specific address. +func (pgb *ChainDB) AddressSpentUnspent(address string) (int64, int64, int64, int64, int64, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + ns, nu, as, au, am, err := RetrieveAddressSpentUnspent(ctx, pgb.db, address) + return ns, nu, as, au, am, pgb.replaceCancelError(err) } -// RetrieveAddressIDsByOutpoint fetches all address row IDs for a given outpoint +// AddressIDsByOutpoint fetches all address row IDs for a given outpoint // (txHash:voutIndex). TODO: Update the vin due to the issue with amountin // invalid for unconfirmed txns. -func (pgb *ChainDB) RetrieveAddressIDsByOutpoint(txHash string, - voutIndex uint32) ([]uint64, []string, int64, error) { - return RetrieveAddressIDsByOutpoint(pgb.db, txHash, voutIndex) +func (pgb *ChainDB) AddressIDsByOutpoint(txHash string, voutIndex uint32) ([]uint64, []string, int64, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + ids, addrs, val, err := RetrieveAddressIDsByOutpoint(ctx, pgb.db, txHash, voutIndex) + return ids, addrs, val, pgb.replaceCancelError(err) } // Update Vin due to DCRD AMOUNTIN - END -// InsightGetAddressTransactions performs a searchrawtransactions for the +// InsightSearchRPCAddressTransactions performs a searchrawtransactions for the // specfied address, max number of transactions, and offset into the transaction // list. The search results are in reverse temporal order. // TODO: Does this really need all the prev vout extra data? -func (pgb *ChainDBRPC) InsightGetAddressTransactions(addr string, count, +func (pgb *ChainDBRPC) InsightSearchRPCAddressTransactions(addr string, count, skip int) []*dcrjson.SearchRawTransactionsResult { address, err := dcrutil.DecodeAddress(addr) if err != nil { @@ -141,57 +158,60 @@ func makeBlockTransactions(blockVerbose *dcrjson.GetBlockVerboseResult) *apitype // GetBlockHash returns the hash of the block at the specified height. TODO: // create GetBlockHashes to return all blocks at a given height. func (pgb *ChainDB) GetBlockHash(idx int64) (string, error) { - hash, err := RetrieveBlockHash(pgb.db, idx) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + hash, err := RetrieveBlockHash(ctx, pgb.db, idx) if err != nil { log.Errorf("Unable to get block hash for block number %d: %v", idx, err) - return "", err + return "", pgb.replaceCancelError(err) } return hash, nil } -// GetAddressBalance returns a *explorer.AddressBalance for the specified -// address, transaction count limit, and transaction number offset. -func (pgb *ChainDB) GetAddressBalance(address string, N, offset int64) *explorer.AddressBalance { +// AddressBalance returns a *explorer.AddressBalance for the specified address, +// transaction count limit, and transaction number offset. +func (pgb *ChainDB) AddressBalance(address string, N, offset int64) (*explorer.AddressBalance, error) { _, balance, err := pgb.AddressHistoryAll(address, N, offset) if err != nil { - return nil + return nil, err } - return balance + return balance, nil } -// GetBlockSummaryTimeRange returns the blocks created within a specified time +// BlockSummaryTimeRange returns the blocks created within a specified time // range (min, max time), up to limit transactions. -func (pgb *ChainDB) GetBlockSummaryTimeRange(min, max int64, limit int) []dbtypes.BlockDataBasic { - blockSummary, err := RetrieveBlockSummaryByTimeRange(pgb.db, min, max, limit) - if err != nil { - log.Errorf("Unable to retrieve block summary using time %d: %v", min, err) - } - return blockSummary +func (pgb *ChainDB) BlockSummaryTimeRange(min, max int64, limit int) ([]dbtypes.BlockDataBasic, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + blockSummary, err := RetrieveBlockSummaryByTimeRange(ctx, pgb.db, min, max, limit) + return blockSummary, pgb.replaceCancelError(err) } -// GetAddressUTXO returns the unspent transaction outputs (UTXOs) paying to the +// AddressUTXO returns the unspent transaction outputs (UTXOs) paying to the // specified address in a []apitypes.AddressTxnOutput. -func (pgb *ChainDB) GetAddressUTXO(address string) []apitypes.AddressTxnOutput { - blockHeight, _, _, err := RetrieveBestBlockHeight(pgb.db) +func (pgb *ChainDB) AddressUTXO(address string) ([]apitypes.AddressTxnOutput, error) { + blockHeight, err := pgb.HeightDB() if err != nil { - log.Error(err) - return nil + return nil, err } - txnOutput, err := RetrieveAddressUTXOs(pgb.db, address, int64(blockHeight)) + + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + txnOutput, err := RetrieveAddressUTXOs(ctx, pgb.db, address, int64(blockHeight)) if err != nil { - log.Error(err) - return nil + return nil, pgb.replaceCancelError(err) } - return txnOutput + return txnOutput, nil } -// GetSpendDetailsByFundingHash will return the spending details (tx, index, -// block height) by funding transaction -func (pgb *ChainDB) GetSpendDetailsByFundingHash(fundHash string) []*apitypes.SpendByFundingHash { - AddrRow, err := RetrieveSpendingTxsByFundingTxWithBlockHeight(pgb.db, fundHash) +// SpendDetailsForFundingTx will return the details of any spending transactions +// (tx, index, block height) for a given funding transaction. +func (pgb *ChainDB) SpendDetailsForFundingTx(fundHash string) ([]*apitypes.SpendByFundingHash, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + addrRow, err := RetrieveSpendingTxsByFundingTxWithBlockHeight(ctx, pgb.db, fundHash) if err != nil { - log.Error(err) - return nil + return nil, pgb.replaceCancelError(err) } - return AddrRow + return addrRow, nil } diff --git a/db/dcrpg/internal/addrstmts.go b/db/dcrpg/internal/addrstmts.go index d9430acd0..59ba242f3 100644 --- a/db/dcrpg/internal/addrstmts.go +++ b/db/dcrpg/internal/addrstmts.go @@ -83,7 +83,6 @@ const ( tx_vin_vout_index, block_time, tx_vin_vout_row_id, value, is_funding` SelectAddressAllByAddress = `SELECT ` + addrsColumnNames + ` FROM addresses WHERE address=$1 ORDER BY block_time DESC;` - SelectAddressRecvCount = `SELECT COUNT(*) FROM addresses WHERE address=$1 AND valid_mainchain = TRUE;` SelectAddressesAllTxn = `SELECT transactions.tx_hash, diff --git a/db/dcrpg/internal/vinoutstmts.go b/db/dcrpg/internal/vinoutstmts.go index fd282f531..e2acb53cf 100644 --- a/db/dcrpg/internal/vinoutstmts.go +++ b/db/dcrpg/internal/vinoutstmts.go @@ -202,8 +202,12 @@ const ( SelectPkScriptByID = `SELECT version, pkscript FROM vouts WHERE id=$1;` SelectPkScriptByOutpoint = `SELECT version, pkscript FROM vouts WHERE tx_hash=$1 and tx_index=$2;` - SelectVoutIDByOutpoint = `SELECT id FROM vouts WHERE tx_hash=$1 and tx_index=$2;` - SelectVoutByID = `SELECT * FROM vouts WHERE id=$1;` + SelectPkScriptByVinID = `SELECT version, pkscript FROM vouts + JOIN vins ON vouts.tx_hash=vins.prev_tx_hash and vouts.tx_index=vins.prev_tx_index + WHERE vins.id=$1;` + + SelectVoutIDByOutpoint = `SELECT id FROM vouts WHERE tx_hash=$1 and tx_index=$2;` + SelectVoutByID = `SELECT * FROM vouts WHERE id=$1;` RetrieveVoutValue = `SELECT value FROM vouts WHERE tx_hash=$1 and tx_index=$2;` RetrieveVoutValues = `SELECT value, tx_index, tx_tree FROM vouts WHERE tx_hash=$1;` diff --git a/db/dcrpg/pgblockchain.go b/db/dcrpg/pgblockchain.go index 75eb2937f..b8343972f 100644 --- a/db/dcrpg/pgblockchain.go +++ b/db/dcrpg/pgblockchain.go @@ -191,6 +191,7 @@ func (u *utxoStore) Size() (sz int) { // blockchain data in a PostgreSQL database. type ChainDB struct { ctx context.Context + queryTimeout time.Duration db *sql.DB chainParams *chaincfg.Params devAddress string @@ -209,6 +210,31 @@ type ChainDB struct { utxoCache utxoStore } +func (pgb *ChainDB) timeoutError() string { + return fmt.Sprintf("%s after %v", dbtypes.TimeoutPrefix, pgb.queryTimeout) +} + +// replaceCancelError will replace the generic error strings that can occur when +// a PG query is canceled (dbtypes.PGCancelError) or a context deadline is +// exceeded (dbtypes.CtxDeadlineExceeded from context.DeadlineExceeded). +func (pgb *ChainDB) replaceCancelError(err error) error { + if err == nil { + return err + } + + patched := err.Error() + if strings.Contains(patched, dbtypes.PGCancelError) { + patched = strings.Replace(patched, dbtypes.PGCancelError, + pgb.timeoutError(), -1) + } else if strings.Contains(patched, dbtypes.CtxDeadlineExceeded) { + patched = strings.Replace(patched, dbtypes.CtxDeadlineExceeded, + pgb.timeoutError(), -1) + } else { + return err + } + return errors.New(patched) +} + // ChainDBRPC provides an interface for storing and manipulating extracted and // includes the RPC Client blockchain data in a PostgreSQL database. type ChainDBRPC struct { @@ -350,7 +376,7 @@ func (t *TicketTxnIDGetter) TxnDbID(txid string, expire bool) (uint64, error) { } // Cache miss. Get the row id by hash from the tickets table. log.Tracef("Cache miss for %s.", txid) - return RetrieveTicketIDByHash(t.db, txid) + return RetrieveTicketIDByHashNoCancel(t.db, txid) } // Set stores the (transaction hash, DB row ID) pair a map for future access. @@ -386,34 +412,40 @@ func NewTicketTxnIDGetter(db *sql.DB) *TicketTxnIDGetter { // DBInfo holds the PostgreSQL database connection information. type DBInfo struct { Host, Port, User, Pass, DBName string + QueryTimeout time.Duration } -// NewChainDBWithCancel constructs a cancellation-capable ChainDB for the given -// connection and Decred network parameters. By default, duplicate row checks on -// insertion are enabled. -func NewChainDBWithCancel(ctx context.Context, dbi *DBInfo, params *chaincfg.Params, +// NewChainDB constructs a ChainDB for the given connection and Decred network +// parameters. By default, duplicate row checks on insertion are enabled. See +// NewChainDBWithCancel to enable context cancellation of running queries. +func NewChainDB(dbi *DBInfo, params *chaincfg.Params, stakeDB *stakedb.StakeDatabase, devPrefetch bool) (*ChainDB, error) { - chainDB, err := NewChainDB(dbi, params, stakeDB, devPrefetch) + ctx := context.Background() + chainDB, err := NewChainDBWithCancel(ctx, dbi, params, stakeDB, devPrefetch) if err != nil { return nil, err } - chainDB.ctx = context.Background() return chainDB, nil } -// NewChainDB constructs a ChainDB for the given connection and Decred network -// parameters. By default, duplicate row checks on insertion are enabled. -func NewChainDB(dbi *DBInfo, params *chaincfg.Params, stakeDB *stakedb.StakeDatabase, +// NewChainDBWithCancel constructs a cancellation-capable ChainDB for the given +// connection and Decred network parameters. By default, duplicate row checks on +// insertion are enabled. See EnableDuplicateCheckOnInsert to change this +// behavior. NewChainDB creates context that cannot be cancelled +// (context.Background()) except by the pg timeouts. If it is necessary to +// cancel queries with CTRL+C, for example, use NewChainDBWithCancel. +func NewChainDBWithCancel(ctx context.Context, dbi *DBInfo, params *chaincfg.Params, stakeDB *stakedb.StakeDatabase, devPrefetch bool) (*ChainDB, error) { - // Connect to the PostgreSQL daemon and return the *sql.DB + // Connect to the PostgreSQL daemon and return the *sql.DB. db, err := Connect(dbi.Host, dbi.Port, dbi.User, dbi.Pass, dbi.DBName) if err != nil { return nil, err } + // Attempt to get DB best block height from tables, but if the tables are // empty or not yet created, it is not an error. - bestHeight, bestHash, _, err := RetrieveBestBlockHeight(db) + bestHeight, bestHash, _, err := RetrieveBestBlockHeight(ctx, db) if err != nil && !(err == sql.ErrNoRows || strings.HasSuffix(err.Error(), "does not exist")) { return nil, err @@ -433,7 +465,7 @@ func NewChainDB(dbi *DBInfo, params *chaincfg.Params, stakeDB *stakedb.StakeData log.Infof("Pre-loading unspent ticket info for InsertVote optimization.") unspentTicketCache := NewTicketTxnIDGetter(db) - unspentTicketDbIDs, unspentTicketHashes, err := RetrieveUnspentTickets(db) + unspentTicketDbIDs, unspentTicketHashes, err := RetrieveUnspentTickets(ctx, db) if err != nil && err != sql.ErrNoRows && !strings.HasSuffix(err.Error(), "does not exist") { return nil, err } @@ -448,7 +480,18 @@ func NewChainDB(dbi *DBInfo, params *chaincfg.Params, stakeDB *stakedb.StakeData tpUpdatePermissions[g] = new(trylock.Mutex) } + // If a query timeout is not set (i.e. zero), default to 24 hrs for + // essentially no timeout. + queryTimeout := dbi.QueryTimeout + if queryTimeout <= 0 { + queryTimeout = time.Hour + } + + log.Infof("Setting PostgreSQL DB statement timeout to %v.", queryTimeout) + return &ChainDB{ + ctx: ctx, + queryTimeout: queryTimeout, db: db, chainParams: params, devAddress: devSubsidyAddress, @@ -546,36 +589,83 @@ func (pgb *ChainDB) DropTables() { // SideChainBlocks retrieves all known side chain blocks. func (pgb *ChainDB) SideChainBlocks() ([]*dbtypes.BlockStatus, error) { - return RetrieveSideChainBlocks(pgb.db) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + scb, err := RetrieveSideChainBlocks(ctx, pgb.db) + return scb, pgb.replaceCancelError(err) } // SideChainTips retrieves the tip/head block for all known side chains. func (pgb *ChainDB) SideChainTips() ([]*dbtypes.BlockStatus, error) { - return RetrieveSideChainTips(pgb.db) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + sct, err := RetrieveSideChainTips(ctx, pgb.db) + return sct, pgb.replaceCancelError(err) } // DisapprovedBlocks retrieves all blocks disapproved by stakeholder votes. func (pgb *ChainDB) DisapprovedBlocks() ([]*dbtypes.BlockStatus, error) { - return RetrieveDisapprovedBlocks(pgb.db) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + disb, err := RetrieveDisapprovedBlocks(ctx, pgb.db) + return disb, pgb.replaceCancelError(err) } // BlockStatus retrieves the block chain status of the specified block. func (pgb *ChainDB) BlockStatus(hash string) (dbtypes.BlockStatus, error) { - return RetrieveBlockStatus(pgb.db, hash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + bs, err := RetrieveBlockStatus(ctx, pgb.db, hash) + return bs, pgb.replaceCancelError(err) +} + +// blockFlags retrieves the block's isValid and isMainchain flags. +func (pgb *ChainDB) blockFlags(ctx context.Context, hash string) (bool, bool, error) { + iv, im, err := RetrieveBlockFlags(ctx, pgb.db, hash) + return iv, im, pgb.replaceCancelError(err) } // BlockFlags retrieves the block's isValid and isMainchain flags. func (pgb *ChainDB) BlockFlags(hash string) (bool, bool, error) { - return RetrieveBlockFlags(pgb.db, hash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + return pgb.blockFlags(ctx, hash) +} + +// BlockFlagsNoCancel retrieves the block's isValid and isMainchain flags. +func (pgb *ChainDB) BlockFlagsNoCancel(hash string) (bool, bool, error) { + return pgb.blockFlags(context.Background(), hash) +} + +// blockChainDbID gets the row ID of the given block hash in the block_chain +// table. The cancellation context is used without timeout. +func (pgb *ChainDB) blockChainDbID(ctx context.Context, hash string) (dbID uint64, err error) { + err = pgb.db.QueryRowContext(ctx, internal.SelectBlockChainRowIDByHash, hash).Scan(&dbID) + err = pgb.replaceCancelError(err) + return +} + +// BlockChainDbID gets the row ID of the given block hash in the block_chain +// table. The cancellation context is used without timeout. +func (pgb *ChainDB) BlockChainDbID(hash string) (dbID uint64, err error) { + return pgb.blockChainDbID(pgb.ctx, hash) +} + +// BlockChainDbIDNoCancel gets the row ID of the given block hash in the +// block_chain table. The cancellation context is used without timeout. +func (pgb *ChainDB) BlockChainDbIDNoCancel(hash string) (dbID uint64, err error) { + return pgb.blockChainDbID(context.Background(), hash) } // TransactionBlocks retrieves the blocks in which the specified transaction // appears, along with the index of the transaction in each of the blocks. The // next and previous block hashes are NOT SET in each BlockStatus. func (pgb *ChainDB) TransactionBlocks(txHash string) ([]*dbtypes.BlockStatus, []uint32, error) { - hashes, heights, inds, valids, mainchains, err := RetrieveTxnsBlocks(pgb.db, txHash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + hashes, heights, inds, valids, mainchains, err := RetrieveTxnsBlocks(ctx, pgb.db, txHash) if err != nil { - return nil, nil, err + return nil, nil, pgb.replaceCancelError(err) } blocks := make([]*dbtypes.BlockStatus, len(hashes)) @@ -595,14 +685,26 @@ func (pgb *ChainDB) TransactionBlocks(txHash string) ([]*dbtypes.BlockStatus, [] // HeightDB queries the DB for the best block height. func (pgb *ChainDB) HeightDB() (uint64, error) { - bestHeight, _, _, err := RetrieveBestBlockHeight(pgb.db) - return bestHeight, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + bestHeight, _, _, err := RetrieveBestBlockHeight(ctx, pgb.db) + return bestHeight, pgb.replaceCancelError(err) } // HashDB queries the DB for the best block's hash. func (pgb *ChainDB) HashDB() (string, error) { - _, bestHash, _, err := RetrieveBestBlockHeight(pgb.db) - return bestHash, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, bestHash, _, err := RetrieveBestBlockHeight(ctx, pgb.db) + return bestHash, pgb.replaceCancelError(err) +} + +// HeightHashDB queries the DB for the best block's height and hash. +func (pgb *ChainDB) HeightHashDB() (uint64, string, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + height, hash, _, err := RetrieveBestBlockHeight(ctx, pgb.db) + return height, hash, pgb.replaceCancelError(err) } // Height uses the last stored height. @@ -624,20 +726,29 @@ func (pgb *ChainDB) Hash() *chainhash.Hash { // BlockHeight queries the DB for the height of the specified hash. func (pgb *ChainDB) BlockHeight(hash string) (int64, error) { - return RetrieveBlockHeight(pgb.db, hash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + height, err := RetrieveBlockHeight(ctx, pgb.db, hash) + return height, pgb.replaceCancelError(err) } // BlockHash queries the DB for the hash of the mainchain block at the given // height. func (pgb *ChainDB) BlockHash(height int64) (string, error) { - return RetrieveBlockHash(pgb.db, height) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + hash, err := RetrieveBlockHash(ctx, pgb.db, height) + return hash, pgb.replaceCancelError(err) } // VotesInBlock returns the number of votes mined in the block with the // specified hash. func (pgb *ChainDB) VotesInBlock(hash string) (int16, error) { - voters, err := RetrieveBlockVoteCount(pgb.db, hash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + voters, err := RetrieveBlockVoteCount(ctx, pgb.db, hash) if err != nil { + err = pgb.replaceCancelError(err) log.Errorf("Unable to get block voter count for hash %s: %v", hash, err) return -1, err } @@ -649,8 +760,10 @@ func (pgb *ChainDB) VotesInBlock(hash string) (int16, error) { // tx input indexes, and the corresponding funding tx output indexes, and an // error value are returned. func (pgb *ChainDB) SpendingTransactions(fundingTxID string) ([]string, []uint32, []uint32, error) { - _, spendingTxns, vinInds, voutInds, err := RetrieveSpendingTxsByFundingTx(pgb.db, fundingTxID) - return spendingTxns, vinInds, voutInds, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, spendingTxns, vinInds, voutInds, err := RetrieveSpendingTxsByFundingTx(ctx, pgb.db, fundingTxID) + return spendingTxns, vinInds, voutInds, pgb.replaceCancelError(err) } // SpendingTransaction returns the transaction that spends the specified @@ -658,46 +771,55 @@ func (pgb *ChainDB) SpendingTransactions(fundingTxID string) ([]string, []uint32 // index, tx tree, and an error value are returned. func (pgb *ChainDB) SpendingTransaction(fundingTxID string, fundingTxVout uint32) (string, uint32, int8, error) { - _, spendingTx, vinInd, tree, err := RetrieveSpendingTxByTxOut(pgb.db, fundingTxID, fundingTxVout) - return spendingTx, vinInd, tree, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, spendingTx, vinInd, tree, err := RetrieveSpendingTxByTxOut(ctx, pgb.db, fundingTxID, fundingTxVout) + return spendingTx, vinInd, tree, pgb.replaceCancelError(err) } // BlockTransactions retrieves all transactions in the specified block, their // indexes in the block, their tree, and an error value. func (pgb *ChainDB) BlockTransactions(blockHash string) ([]string, []uint32, []int8, error) { - _, blockTransactions, blockInds, trees, _, err := RetrieveTxsByBlockHash(pgb.db, blockHash) - return blockTransactions, blockInds, trees, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, blockTransactions, blockInds, trees, _, err := RetrieveTxsByBlockHash(ctx, pgb.db, blockHash) + return blockTransactions, blockInds, trees, pgb.replaceCancelError(err) } // Transaction retrieves all rows from the transactions table for the given // transaction hash. func (pgb *ChainDB) Transaction(txHash string) ([]*dbtypes.Tx, error) { - _, dbTxs, err := RetrieveDbTxsByHash(pgb.db, txHash) - return dbTxs, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, dbTxs, err := RetrieveDbTxsByHash(ctx, pgb.db, txHash) + return dbTxs, pgb.replaceCancelError(err) } // BlockMissedVotes retrieves the ticket IDs for all missed votes in the // specified block, and an error value. func (pgb *ChainDB) BlockMissedVotes(blockHash string) ([]string, error) { - return RetrieveMissedVotesInBlock(pgb.db, blockHash) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + mv, err := RetrieveMissedVotesInBlock(ctx, pgb.db, blockHash) + return mv, pgb.replaceCancelError(err) } // PoolStatusForTicket retrieves the specified ticket's spend status and ticket // pool status, and an error value. func (pgb *ChainDB) PoolStatusForTicket(txid string) (dbtypes.TicketSpendType, dbtypes.TicketPoolStatus, error) { - _, spendType, poolStatus, err := RetrieveTicketStatusByHash(pgb.db, txid) - return spendType, poolStatus, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, spendType, poolStatus, err := RetrieveTicketStatusByHash(ctx, pgb.db, txid) + return spendType, poolStatus, pgb.replaceCancelError(err) } // VoutValue retrieves the value of the specified transaction outpoint in atoms. func (pgb *ChainDB) VoutValue(txID string, vout uint32) (uint64, error) { - // txDbID, _, _, err := RetrieveTxByHash(pgb.db, txID) - // if err != nil { - // return 0, fmt.Errorf("RetrieveTxByHash: %v", err) - // } - voutValue, err := RetrieveVoutValue(pgb.db, txID, vout) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + voutValue, err := RetrieveVoutValue(ctx, pgb.db, txID, vout) if err != nil { - return 0, fmt.Errorf("RetrieveVoutValue: %v", err) + return 0, pgb.replaceCancelError(err) } return voutValue, nil } @@ -706,13 +828,11 @@ func (pgb *ChainDB) VoutValue(txID string, vout uint32) (uint64, error) { // transaction. The corresponding indexes in the block and tx trees of the // outpoints, and an error value are also returned. func (pgb *ChainDB) VoutValues(txID string) ([]uint64, []uint32, []int8, error) { - // txDbID, _, _, err := RetrieveTxByHash(pgb.db, txID) - // if err != nil { - // return nil, fmt.Errorf("RetrieveTxByHash: %v", err) - // } - voutValues, txInds, txTrees, err := RetrieveVoutValues(pgb.db, txID) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + voutValues, txInds, txTrees, err := RetrieveVoutValues(ctx, pgb.db, txID) if err != nil { - return nil, nil, nil, fmt.Errorf("RetrieveVoutValues: %v", err) + return nil, nil, nil, pgb.replaceCancelError(err) } return voutValues, txInds, txTrees, nil } @@ -721,49 +841,67 @@ func (pgb *ChainDB) VoutValues(txID string) ([]uint64, []uint32, []int8, error) // transaction. The index of the transaction within the block, the transaction // index, and an error value are also returned. func (pgb *ChainDB) TransactionBlock(txID string) (string, uint32, int8, error) { - _, blockHash, blockInd, tree, err := RetrieveTxByHash(pgb.db, txID) - return blockHash, blockInd, tree, err + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, blockHash, blockInd, tree, err := RetrieveTxByHash(ctx, pgb.db, txID) + return blockHash, blockInd, tree, pgb.replaceCancelError(err) } // AgendaVotes fetches the data used to plot a graph of votes cast per day per // choice for the provided agenda. func (pgb *ChainDB) AgendaVotes(agendaID string, chartType int) (*dbtypes.AgendaVoteChoices, error) { - return retrieveAgendaVoteChoices(pgb.db, agendaID, chartType) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + avc, err := retrieveAgendaVoteChoices(ctx, pgb.db, agendaID, chartType) + return avc, pgb.replaceCancelError(err) } -// GetAddressMetrics returns the block time of the oldest transaction and the +// NumAddressIntervals gets the number of unique time intervals for the +// specified grouping where there are entries in the addresses table for the +// given address. +func (pgb *ChainDB) NumAddressIntervals(addr string, grouping dbtypes.TimeBasedGrouping) (int64, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + return retrieveAddressTxsCount(ctx, pgb.db, addr, grouping.String()) +} + +// AddressMetrics returns the block time of the oldest transaction and the // total count for all the transactions linked to the provided address grouped // by years, months, weeks and days time grouping in seconds. // This helps plot more meaningful address history graphs to the user. -func (pgb *ChainDB) GetAddressMetrics(addr string) (*dbtypes.AddressMetrics, error) { +func (pgb *ChainDB) AddressMetrics(addr string) (*dbtypes.AddressMetrics, error) { + // For each time grouping/interval size, get the number if intervals with + // data for the address. var metrics dbtypes.AddressMetrics - - for _, s := range []dbtypes.TimeBasedGrouping{dbtypes.YearGrouping, - dbtypes.MonthGrouping, dbtypes.WeekGrouping, dbtypes.DayGrouping} { - - txCount, err := retrieveAddressTxsCount(pgb.db, addr, s.String()) + for _, s := range dbtypes.TimeIntervals { + numIntervals, err := pgb.NumAddressIntervals(addr, s) if err != nil { return nil, fmt.Errorf("retrieveAddressAllTxsCount failed: error: %v", err) } + switch s { case dbtypes.YearGrouping: - metrics.YearTxsCount = txCount + metrics.YearTxsCount = numIntervals case dbtypes.MonthGrouping: - metrics.MonthTxsCount = txCount + metrics.MonthTxsCount = numIntervals case dbtypes.WeekGrouping: - metrics.WeekTxsCount = txCount + metrics.WeekTxsCount = numIntervals case dbtypes.DayGrouping: - metrics.DayTxsCount = txCount + metrics.DayTxsCount = numIntervals } } - blockTime, err := retrieveOldestTxBlockTime(pgb.db, addr) + // Get the time of the block with the first transaction involving the + // address (oldest transaction block time). + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + blockTime, err := retrieveOldestTxBlockTime(ctx, pgb.db, addr) if err != nil { return nil, fmt.Errorf("retrieveOldestTxBlockTime failed: error: %v", err) } - metrics.OldestBlockTime = blockTime - return &metrics, err + + return &metrics, pgb.replaceCancelError(err) } // AddressTransactions retrieves a slice of *dbtypes.AddressRow for a given @@ -772,7 +910,7 @@ func (pgb *ChainDB) GetAddressMetrics(addr string) (*dbtypes.AddressMetrics, err // txnType transactions. func (pgb *ChainDB) AddressTransactions(address string, N, offset int64, txnType dbtypes.AddrTxnType) (addressRows []*dbtypes.AddressRow, err error) { - var addrFunc func(*sql.DB, string, int64, int64) ([]uint64, []*dbtypes.AddressRow, error) + var addrFunc func(context.Context, *sql.DB, string, int64, int64) ([]uint64, []*dbtypes.AddressRow, error) switch txnType { case dbtypes.AddrTxnCredit: addrFunc = RetrieveAddressCreditTxns @@ -787,7 +925,11 @@ func (pgb *ChainDB) AddressTransactions(address string, N, offset int64, return nil, fmt.Errorf("unknown AddrTxnType %v", txnType) } - _, addressRows, err = addrFunc(pgb.db, address, N, offset) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + + _, addressRows, err = addrFunc(ctx, pgb.db, address, N, offset) + err = pgb.replaceCancelError(err) return } @@ -797,35 +939,45 @@ func (pgb *ChainDB) AddressHistoryAll(address string, N, offset int64) ([]*dbtyp return pgb.AddressHistory(address, N, offset, dbtypes.AddrTxnAll) } -// GetTicketPoolBlockMaturity returns the block at which all tickets with height +// TicketPoolBlockMaturity returns the block at which all tickets with height // greater than it are immature. -func (pgb *ChainDB) GetTicketPoolBlockMaturity() int64 { +func (pgb *ChainDB) TicketPoolBlockMaturity() int64 { bestBlock := int64(pgb.stakeDB.Height()) return bestBlock - int64(pgb.chainParams.TicketMaturity) } -// GetTicketPoolByDateAndInterval fetches the tickets ordered by the purchase date +// TicketPoolByDateAndInterval fetches the tickets ordered by the purchase date // interval provided and an error value. -func (pgb *ChainDB) GetTicketPoolByDateAndInterval(maturityBlock int64, +func (pgb *ChainDB) TicketPoolByDateAndInterval(maturityBlock int64, interval dbtypes.TimeBasedGrouping) (*dbtypes.PoolTicketsData, error) { - - return retrieveTicketsByDate(pgb.db, maturityBlock, interval.String()) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + tpd, err := retrieveTicketsByDate(ctx, pgb.db, maturityBlock, interval.String()) + return tpd, pgb.replaceCancelError(err) } -// PosIntervals retrieves the blocks at the respective stakebase windows interval. -// The term "window" is used here to describe the group of blocks whose count is -// defined by chainParams.StakeDiffWindowSize. During this chainParams.StakeDiffWindowSize -// block interval the ticket price and the difficulty value is constant. +// PosIntervals retrieves the blocks at the respective stakebase windows +// interval. The term "window" is used here to describe the group of blocks +// whose count is defined by chainParams.StakeDiffWindowSize. During this +// chainParams.StakeDiffWindowSize block interval the ticket price and the +// difficulty value is constant. func (pgb *ChainDB) PosIntervals(limit, offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { - return retrieveWindowBlocks(pgb.db, pgb.chainParams.StakeDiffWindowSize, limit, offset) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + bgi, err := retrieveWindowBlocks(ctx, pgb.db, pgb.chainParams.StakeDiffWindowSize, limit, offset) + return bgi, pgb.replaceCancelError(err) } -// TimeBasedIntervals retrieves blocks groups by the selected time-based interval. -// For the consecutive groups the number of blocks grouped together is not uniform. +// TimeBasedIntervals retrieves blocks groups by the selected time-based +// interval. For the consecutive groups the number of blocks grouped together is +// not uniform. func (pgb *ChainDB) TimeBasedIntervals(timeGrouping dbtypes.TimeBasedGrouping, limit, offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { - - return retrieveTimeBasedBlockListing(pgb.db, timeGrouping.String(), limit, offset) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + bgi, err := retrieveTimeBasedBlockListing(ctx, pgb.db, timeGrouping.String(), + limit, offset) + return bgi, pgb.replaceCancelError(err) } // TicketPoolVisualization helps block consecutive and duplicate DB queries for @@ -902,16 +1054,16 @@ func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) for { // Latest block where mature tickets may have been mined. - maturityBlock := pgb.GetTicketPoolBlockMaturity() + maturityBlock := pgb.TicketPoolBlockMaturity() // Tickets grouped by time interval - ticketsByTime, err := pgb.GetTicketPoolByDateAndInterval(maturityBlock, interval) + ticketsByTime, err := pgb.TicketPoolByDateAndInterval(maturityBlock, interval) if err != nil { return nil, nil, 0, err } // Tickets grouped by price - ticketsByPrice, err := retrieveTicketByPrice(pgb.db, maturityBlock) + ticketsByPrice, err := pgb.TicketsByPrice(maturityBlock) if err != nil { return nil, nil, 0, err } @@ -920,7 +1072,7 @@ func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) byTimeAndPrice = []*dbtypes.PoolTicketsData{ticketsByTime, ticketsByPrice} // Tickets grouped by number of inputs. - byInputs, err = retrieveTicketsGroupedByType(pgb.db) + byInputs, err = pgb.TicketsByInputCount() if err != nil { return nil, nil, 0, err } @@ -938,7 +1090,7 @@ func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) // retrieveDevBalance retrieves a new DevFundBalance without regard to the cache func (pgb *ChainDB) retrieveDevBalance() (*DevFundBalance, error) { - bb, hash, _, err := RetrieveBestBlockHeight(pgb.db) + bb, hash, err := pgb.HeightHashDB() if err != nil { return nil, err } @@ -970,6 +1122,7 @@ func (pgb *ChainDB) FreshenAddressCaches(lazyProjectFund bool) error { updateBalance := func() error { log.Infof("Pre-fetching project fund balance at height %d...", pgb.bestBlock) if _, err := pgb.UpdateDevBalance(); err != nil { + err = pgb.replaceCancelError(err) return fmt.Errorf("Failed to update project fund balance: %v", err) } return nil @@ -1084,7 +1237,7 @@ func (pgb *ChainDB) addressBalance(address string) (*explorer.AddressBalance, er if !fresh { numSpent, numUnspent, amtSpent, amtUnspent, numMergedSpent, err := - RetrieveAddressSpentUnspent(pgb.db, address) + pgb.AddressSpentUnspent(address) if err != nil { return nil, err } @@ -1163,7 +1316,7 @@ func (pgb *ChainDB) AddressHistory(address string, N, offset int64, } else { log.Debugf("Obtaining balance via DB query.") numSpent, numUnspent, amtSpent, amtUnspent, numMergedSpent, err := - RetrieveAddressSpentUnspent(pgb.db, address) + pgb.AddressSpentUnspent(address) if err != nil { return nil, nil, err } @@ -1190,6 +1343,26 @@ func (pgb *ChainDB) AddressHistory(address string, N, offset int64, return addressRows, &balanceInfo, nil } +// DbTxByHash retrieves a row of the transactions table corresponding to the +// given transaction hash. Transactions in valid and mainchain blocks are chosen +// first. +func (pgb *ChainDB) DbTxByHash(txid string) (*dbtypes.Tx, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + _, dbTx, err := RetrieveDbTxByHash(ctx, pgb.db, txid) + return dbTx, pgb.replaceCancelError(err) +} + +// FundingOutpointIndxByVinID retrieves the the transaction output index of the +// previous outpoint for a transaction input specified by row ID in the vins +// table, which stores previous outpoints for each vin. +func (pgb *ChainDB) FundingOutpointIndxByVinID(id uint64) (uint32, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + ind, err := RetrieveFundingOutpointIndxByVinID(ctx, pgb.db, id) + return ind, pgb.replaceCancelError(err) +} + // FillAddressTransactions is used to fill out the transaction details in an // explorer.AddressInfo generated by explorer.ReduceAddressHistory, usually from // the output of AddressHistory. This function also sets the number of @@ -1202,7 +1375,9 @@ func (pgb *ChainDB) FillAddressTransactions(addrInfo *explorer.AddressInfo) erro var numUnconfirmed int64 for i, txn := range addrInfo.Transactions { - _, dbTx, err := RetrieveDbTxByHash(pgb.db, txn.TxID) + // Retrieve the most valid, most mainchain, and most recent tx with this + // hash. This means it prefers mainchain and valid blocks first. + dbTx, err := pgb.DbTxByHash(txn.TxID) if err != nil { return err } @@ -1223,9 +1398,9 @@ func (pgb *ChainDB) FillAddressTransactions(addrInfo *explorer.AddressInfo) erro // along with matching tx hash in the addresses table. if txn.MatchedTx != `` { if !txn.IsFunding { - // Lookup by the database row id - idx, err := RetrieveFundingOutpointIndxByVinID(pgb.db, dbTx.VinDbIds[txn.InOutID]) - + // Spending transaction: lookup the previous outpoint's txout + // index by the vins table row ID. + idx, err := pgb.FundingOutpointIndxByVinID(dbTx.VinDbIds[txn.InOutID]) if err != nil { log.Warnf("Matched Transaction Lookup failed for %s:%d: id: %d: %v", txn.TxID, txn.InOutID, txn.InOutID, err) @@ -1234,9 +1409,9 @@ func (pgb *ChainDB) FillAddressTransactions(addrInfo *explorer.AddressInfo) erro } } else { - // Lookup by the matching tx hash and matching tx index + // Funding transaction: lookup by the matching (spending) tx + // hash and tx index. _, idx, _, err := pgb.SpendingTransaction(txn.TxID, txn.InOutID) - if err != nil { log.Warnf("Matched Transaction Lookup failed for %s:%d: %v", txn.TxID, txn.InOutID, err) @@ -1268,7 +1443,7 @@ func (pgb *ChainDB) AddressTotals(address string) (*apitypes.AddressTotals, erro return nil, err } - bestHeight, bestHash, _, err := RetrieveBestBlockHeight(pgb.db) + bestHeight, bestHash, err := pgb.HeightHashDB() if err != nil { return nil, err } @@ -1284,8 +1459,7 @@ func (pgb *ChainDB) AddressTotals(address string) (*apitypes.AddressTotals, erro }, nil } -func (pgb *ChainDB) addressInfo(addr string, count, skip int64, - txnType dbtypes.AddrTxnType) (*explorer.AddressInfo, *explorer.AddressBalance, error) { +func (pgb *ChainDB) addressInfo(addr string, count, skip int64, txnType dbtypes.AddrTxnType) (*explorer.AddressInfo, *explorer.AddressBalance, error) { address, err := dcrutil.DecodeAddress(addr) if err != nil { log.Infof("Invalid address %s: %v", addr, err) @@ -1426,74 +1600,127 @@ func (pgb *ChainDB) Store(blockData *blockdata.BlockData, msgBlock *wire.MsgBloc return err } -// GetTxHistoryData fetches the address history chart data for the provided parameters. -func (pgb *ChainDB) GetTxHistoryData(address string, addrChart dbtypes.HistoryChart, - chartGroupings dbtypes.TimeBasedGrouping) (*dbtypes.ChartsData, error) { +// TxHistoryData fetches the address history chart data for specified chart +// type and time grouping. +func (pgb *ChainDB) TxHistoryData(address string, addrChart dbtypes.HistoryChart, + chartGroupings dbtypes.TimeBasedGrouping) (cd *dbtypes.ChartsData, err error) { timeInterval := chartGroupings.String() + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + switch addrChart { case dbtypes.TxsType: - return retrieveTxHistoryByType(pgb.db, address, timeInterval) + cd, err = retrieveTxHistoryByType(ctx, pgb.db, address, timeInterval) case dbtypes.AmountFlow: - return retrieveTxHistoryByAmountFlow(pgb.db, address, timeInterval) + cd, err = retrieveTxHistoryByAmountFlow(ctx, pgb.db, address, timeInterval) case dbtypes.TotalUnspent: - return retrieveTxHistoryByUnspentAmount(pgb.db, address, timeInterval) + cd, err = retrieveTxHistoryByUnspentAmount(ctx, pgb.db, address, timeInterval) default: - return nil, fmt.Errorf("unknown error occurred") + err = fmt.Errorf("unknown error occurred") } + err = pgb.replaceCancelError(err) + return } -// GetTicketsPriceByHeight returns the ticket price by height chart data. -// This is the default chart that appears at charts page. -func (pgb *ChainDB) GetTicketsPriceByHeight() (*dbtypes.ChartsData, error) { - d, err := RetrieveTicketsPriceByHeight(pgb.db, pgb.chainParams.StakeDiffWindowSize) +// TicketsPriceByHeight returns the ticket price by height chart data. This is +// the default chart that appears at charts page. +func (pgb *ChainDB) TicketsPriceByHeight() (*dbtypes.ChartsData, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + d, err := RetrieveTicketsPriceByHeight(ctx, pgb.db, pgb.chainParams.StakeDiffWindowSize) if err != nil { - return nil, fmt.Errorf("RetrieveTicketsPriceByHeight: %v", err) + return nil, pgb.replaceCancelError(err) } return &dbtypes.ChartsData{Time: d.Time, ValueF: d.ValueF}, nil } -// GetPgChartsData fetches the charts data that is stored in pg +// TicketsByPrice returns chart data for tickets grouped by price. maturityBlock +// is used to define when tickets are considered live. +func (pgb *ChainDB) TicketsByPrice(maturityBlock int64) (*dbtypes.PoolTicketsData, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + ptd, err := retrieveTicketByPrice(ctx, pgb.db, maturityBlock) + return ptd, pgb.replaceCancelError(err) +} + +// TicketsByInputCount returns chart data for tickets grouped by number of +// inputs. +func (pgb *ChainDB) TicketsByInputCount() (*dbtypes.PoolTicketsData, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + ptd, err := retrieveTicketsGroupedByType(ctx, pgb.db) + return ptd, pgb.replaceCancelError(err) +} + +// CoinSupplyChartsData retrieves the coin supply charts data. +func (pgb *ChainDB) CoinSupplyChartsData() (*dbtypes.ChartsData, error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + cd, err := retrieveCoinSupply(ctx, pgb.db) + return cd, pgb.replaceCancelError(err) +} + +// GetPgChartsData retrieves the different types of charts data. func (pgb *ChainDB) GetPgChartsData() (map[string]*dbtypes.ChartsData, error) { - tickets, err := RetrieveTicketsPriceByHeight(pgb.db, pgb.chainParams.StakeDiffWindowSize) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + tickets, err := RetrieveTicketsPriceByHeight(ctx, pgb.db, pgb.chainParams.StakeDiffWindowSize) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("RetrieveTicketsPriceByHeight: %v", err) } - supply, err := retrieveCoinSupply(pgb.db) + supply, err := pgb.CoinSupplyChartsData() if err != nil { - return nil, fmt.Errorf("retrieveCoinSupply: %v", err) + err = pgb.replaceCancelError(err) + return nil, fmt.Errorf("CoinSupplyChartsData: %v", err) } - size, err := retrieveBlockTicketsPoolValue(pgb.db) + ctx, cancel = context.WithTimeout(pgb.ctx, pgb.queryTimeout) + size, err := retrieveBlockTicketsPoolValue(ctx, pgb.db) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("retrieveBlockTicketsPoolValue: %v", err) } - txRate, err := retrieveTxPerDay(pgb.db) + ctx, cancel = context.WithTimeout(pgb.ctx, pgb.queryTimeout) + txRate, err := retrieveTxPerDay(ctx, pgb.db) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("retrieveTxPerDay: %v", err) } - ticketsSpendType, err := retrieveTicketSpendTypePerBlock(pgb.db) + ctx, cancel = context.WithTimeout(pgb.ctx, pgb.queryTimeout) + ticketsSpendType, err := retrieveTicketSpendTypePerBlock(ctx, pgb.db) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("retrieveTicketSpendTypePerBlock: %v", err) } - ticketsByOutputsAllBlocks, err := retrieveTicketByOutputCount(pgb.db, outputCountByAllBlocks) + ctx, cancel = context.WithTimeout(pgb.ctx, pgb.queryTimeout) + ticketsByOutputsAllBlocks, err := retrieveTicketByOutputCount(ctx, pgb.db, outputCountByAllBlocks) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("retrieveTicketByOutputCount by All Blocks: %v", err) } - ticketsByOutputsTPWindow, err := retrieveTicketByOutputCount(pgb.db, outputCountByTicketPoolWindow) + ctx, cancel = context.WithTimeout(pgb.ctx, pgb.queryTimeout) + ticketsByOutputsTPWindow, err := retrieveTicketByOutputCount(ctx, pgb.db, outputCountByTicketPoolWindow) + cancel() if err != nil { + err = pgb.replaceCancelError(err) return nil, fmt.Errorf("retrieveTicketByOutputCount by All TP window: %v", err) } - var data = map[string]*dbtypes.ChartsData{ + data := map[string]*dbtypes.ChartsData{ "avg-block-size": {Time: size.Time, Size: size.Size}, "blockchain-size": {Time: size.Time, ChainSize: size.ChainSize}, "tx-per-block": {Value: size.Value, Count: size.Count}, @@ -1510,19 +1737,31 @@ func (pgb *ChainDB) GetPgChartsData() (map[string]*dbtypes.ChartsData, error) { return data, nil } +// SetVinsMainchainByBlock first retrieves for all transactions in the specified +// block the vin_db_ids and vout_db_ids arrays, along with mainchain status, +// from the transactions table, and then sets the is_mainchain flag in the vins +// table for each row of vins in the vin_db_ids array. The returns are the +// number of vins updated, the vin row IDs array, the vouts row IDs array, and +// an error value. func (pgb *ChainDB) SetVinsMainchainByBlock(blockHash string) (int64, []dbtypes.UInt64Array, []dbtypes.UInt64Array, error) { - // Get vins DB IDs for the block + // The queries in this function should not timeout or (probably) canceled, + // so use a background context. + ctx := context.Background() + + // Get vins DB IDs from the transactions table, for each tx in the block. onlyRegularTxns := false - vinDbIDsBlk, voutDbIDsBlk, areMainchain, err := RetrieveTxnsVinsVoutsByBlock(pgb.db, blockHash, onlyRegularTxns) + vinDbIDsBlk, voutDbIDsBlk, areMainchain, err := + RetrieveTxnsVinsVoutsByBlock(ctx, pgb.db, blockHash, onlyRegularTxns) if err != nil { return 0, nil, nil, fmt.Errorf("unable to retrieve vin data for block %s: %v", blockHash, err) } + + // Set the is_mainchain flag for each vin. vinsUpdated, err := pgb.setVinsMainchainForMany(vinDbIDsBlk, areMainchain) return vinsUpdated, vinDbIDsBlk, voutDbIDsBlk, err } -func (pgb *ChainDB) setVinsMainchainForMany(vinDbIDsBlk []dbtypes.UInt64Array, - areMainchain []bool) (int64, error) { +func (pgb *ChainDB) setVinsMainchainForMany(vinDbIDsBlk []dbtypes.UInt64Array, areMainchain []bool) (int64, error) { var rowsUpdated int64 // each transaction for it, vs := range vinDbIDsBlk { @@ -1560,30 +1799,59 @@ func (pgb *ChainDB) setVinsMainchainOneTxn(vinDbIDs dbtypes.UInt64Array, return rowsUpdated, nil } +// PkScriptByVinID retrieves the pkScript and script version for the row of the +// vouts table corresponding to the previous output of the vin specified by row +// ID of the vins table. +func (pgb *ChainDB) PkScriptByVinID(id uint64) (pkScript []byte, ver uint16, err error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + pks, ver, err := RetrievePkScriptByVinID(ctx, pgb.db, id) + return pks, ver, pgb.replaceCancelError(err) +} + +// PkScriptByVoutID retrieves the pkScript and script version for the row of the +// vouts table specified by the row ID id. +func (pgb *ChainDB) PkScriptByVoutID(id uint64) (pkScript []byte, ver uint16, err error) { + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + pks, ver, err := RetrievePkScriptByVoutID(ctx, pgb.db, id) + return pks, ver, pgb.replaceCancelError(err) +} + // VinsForTx returns a slice of dbtypes.VinTxProperty values for each vin -// referenced by the transaction dbTx. +// referenced by the transaction dbTx, along with the pkScript and script +// version for the corresponding prevous outpoints. func (pgb *ChainDB) VinsForTx(dbTx *dbtypes.Tx) ([]dbtypes.VinTxProperty, []string, []uint16, error) { + // Retrieve the pkScript and script version for the previous outpoint of + // each vin. var prevPkScripts []string var versions []uint16 for _, id := range dbTx.VinDbIds { - pkScript, ver, err := RetrievePkScriptByID(pgb.db, id) + pkScript, ver, err := pgb.PkScriptByVinID(id) if err != nil { - return nil, nil, nil, fmt.Errorf("RetrievePkScriptByID: %v", err) + return nil, nil, nil, fmt.Errorf("PkScriptByVinID: %v", err) } prevPkScripts = append(prevPkScripts, hex.EncodeToString(pkScript)) versions = append(versions, ver) } - vins, err := RetrieveVinsByIDs(pgb.db, dbTx.VinDbIds) + + // Retrieve the vins row data. + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + vins, err := RetrieveVinsByIDs(ctx, pgb.db, dbTx.VinDbIds) if err != nil { err = fmt.Errorf("RetrieveVinsByIDs: %v", err) } - return vins, prevPkScripts, versions, err + return vins, prevPkScripts, versions, pgb.replaceCancelError(err) } // VoutsForTx returns a slice of dbtypes.Vout values for each vout referenced by // the transaction dbTx. func (pgb *ChainDB) VoutsForTx(dbTx *dbtypes.Tx) ([]dbtypes.Vout, error) { - return RetrieveVoutsByIDs(pgb.db, dbTx.VoutDbIds) + ctx, cancel := context.WithTimeout(pgb.ctx, pgb.queryTimeout) + defer cancel() + vouts, err := RetrieveVoutsByIDs(ctx, pgb.db, dbTx.VoutDbIds) + return vouts, pgb.replaceCancelError(err) } func (pgb *ChainDB) TipToSideChain(mainRoot string) (string, int64, error) { @@ -1826,7 +2094,7 @@ func (pgb *ChainDB) UpdateLastBlock(msgBlock *wire.MsgBlock, isMainchain bool) e // mainchain block or any of its components, or update the block_chain // table to point to this block. if !isMainchain { // only check when current block is side chain - _, lastIsMainchain, err := pgb.BlockFlags(lastBlockHash.String()) + _, lastIsMainchain, err := pgb.BlockFlagsNoCancel(lastBlockHash.String()) if err != nil { log.Errorf("Unable to determine status of previous block %v: %v", lastBlockHash, err) @@ -1848,7 +2116,7 @@ func (pgb *ChainDB) UpdateLastBlock(msgBlock *wire.MsgBlock, isMainchain bool) e log.Debugf("The previous block %s for block %s not found in cache, "+ "looking it up.", lastBlockHash, msgBlock.BlockHash()) var err error - lastBlockDbID, err = RetrieveBlockChainDbID(pgb.db, lastBlockHash.String()) + lastBlockDbID, err = pgb.BlockChainDbIDNoCancel(lastBlockHash.String()) if err != nil { return fmt.Errorf("unable to locate block %s in block_chain table: %v", lastBlockHash, err) @@ -2133,14 +2401,16 @@ func (pgb *ChainDB) storeTxns(msgBlock *MsgBlockPG, txTree int8, for iu := range unspentEnM { t, err0 := unspentTicketCache.TxnDbID(unspentEnM[iu], false) if err0 != nil { - txRes.err = fmt.Errorf("failed to retrieve ticket %s DB ID: %v", unspentEnM[iu], err0) + txRes.err = fmt.Errorf("failed to retrieve ticket %s DB ID: %v", + unspentEnM[iu], err0) return txRes } unspentEnMRowIDs[iu] = t } // Update status of the unspent expired and missed tickets. - numUnrevokedMisses, err := SetPoolStatusForTickets(pgb.db, unspentEnMRowIDs, missStatuses) + numUnrevokedMisses, err := SetPoolStatusForTickets(pgb.db, + unspentEnMRowIDs, missStatuses) if err != nil { log.Errorf("SetPoolStatusForTicketsByHash: %v", err) } else if numUnrevokedMisses > 0 { @@ -2364,9 +2634,13 @@ func (pgb *ChainDB) UpdateSpendingInfoInAllAddresses(barLoad chan *dbtypes.Progr // UpdateSpendingInfoInAllTickets reviews all votes and revokes and sets this // spending info in the tickets table. func (pgb *ChainDB) UpdateSpendingInfoInAllTickets() (int64, error) { + // The queries in this function should not timeout or (probably) canceled, + // so use a background context. + ctx := context.Background() + // Get the full list of votes (DB IDs and heights), and spent ticket hashes allVotesDbIDs, allVotesHeights, ticketDbIDs, err := - RetrieveAllVotesDbIDsHeightsTicketDbIDs(pgb.db) + RetrieveAllVotesDbIDsHeightsTicketDbIDs(ctx, pgb.db) if err != nil { log.Errorf("RetrieveAllVotesDbIDsHeightsTicketDbIDs: %v", err) return 0, err @@ -2390,7 +2664,7 @@ func (pgb *ChainDB) UpdateSpendingInfoInAllTickets() (int64, error) { // Revokes - revokeIDs, _, revokeHeights, vinDbIDs, err := RetrieveAllRevokes(pgb.db) + revokeIDs, _, revokeHeights, vinDbIDs, err := RetrieveAllRevokes(ctx, pgb.db) if err != nil { log.Errorf("RetrieveAllRevokes: %v", err) return 0, err @@ -2398,14 +2672,14 @@ func (pgb *ChainDB) UpdateSpendingInfoInAllTickets() (int64, error) { revokedTicketHashes := make([]string, len(vinDbIDs)) for i, vinDbID := range vinDbIDs { - revokedTicketHashes[i], err = RetrieveFundingTxByVinDbID(pgb.db, vinDbID) + revokedTicketHashes[i], err = RetrieveFundingTxByVinDbID(ctx, pgb.db, vinDbID) if err != nil { log.Errorf("RetrieveFundingTxByVinDbID: %v", err) return 0, err } } - revokedTicketDbIDs, err := RetrieveTicketIDsByHashes(pgb.db, revokedTicketHashes) + revokedTicketDbIDs, err := RetrieveTicketIDsByHashes(ctx, pgb.db, revokedTicketHashes) if err != nil { log.Errorf("RetrieveTicketIDsByHashes: %v", err) return 0, err diff --git a/db/dcrpg/queries.go b/db/dcrpg/queries.go index 75d834491..23aabff31 100644 --- a/db/dcrpg/queries.go +++ b/db/dcrpg/queries.go @@ -495,9 +495,9 @@ func InsertVotes(db *sql.DB, dbTxns []*dbtypes.Tx, _ /*txDbIDs*/ []uint64, fTx * // RetrieveMissedVotesInBlock gets a list of ticket hashes that were called to // vote in the given block, but missed their vote. -func RetrieveMissedVotesInBlock(db *sql.DB, blockHash string) (ticketHashes []string, err error) { +func RetrieveMissedVotesInBlock(ctx context.Context, db *sql.DB, blockHash string) (ticketHashes []string, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectMissesInBlock, blockHash) + rows, err = db.QueryContext(ctx, internal.SelectMissesInBlock, blockHash) if err != nil { return nil, err } @@ -520,9 +520,11 @@ func RetrieveMissedVotesInBlock(db *sql.DB, blockHash string) (ticketHashes []st // keys), transaction hashes, block heights. It also gets the row ID in the vins // table for the first input of the revocation transaction, which should // correspond to the stakesubmission previous outpoint of the ticket purchase. -func RetrieveAllRevokes(db *sql.DB) (ids []uint64, hashes []string, heights []int64, vinDbIDs []uint64, err error) { +// This function is used in UpdateSpendingInfoInAllTickets, so it should not be +// subject to timeouts. +func RetrieveAllRevokes(ctx context.Context, db *sql.DB) (ids []uint64, hashes []string, heights []int64, vinDbIDs []uint64, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectAllRevokes) + rows, err = db.QueryContext(ctx, internal.SelectAllRevokes) if err != nil { return nil, nil, nil, nil, err } @@ -548,11 +550,12 @@ func RetrieveAllRevokes(db *sql.DB) (ids []uint64, hashes []string, heights []in // RetrieveAllVotesDbIDsHeightsTicketDbIDs gets for all votes the row IDs // (primary keys) in the votes table, the block heights, and the row IDs in the -// tickets table of the spent tickets. -func RetrieveAllVotesDbIDsHeightsTicketDbIDs(db *sql.DB) (ids []uint64, heights []int64, +// tickets table of the spent tickets. This function is used in +// UpdateSpendingInfoInAllTickets, so it should not be subject to timeouts. +func RetrieveAllVotesDbIDsHeightsTicketDbIDs(ctx context.Context, db *sql.DB) (ids []uint64, heights []int64, ticketDbIDs []uint64, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectAllVoteDbIDsHeightsTicketDbIDs) + rows, err = db.QueryContext(ctx, internal.SelectAllVoteDbIDsHeightsTicketDbIDs) if err != nil { return nil, nil, nil, err } @@ -575,9 +578,8 @@ func RetrieveAllVotesDbIDsHeightsTicketDbIDs(db *sql.DB) (ids []uint64, heights // retrieveWindowBlocks fetches chunks of windows using the limit and offset provided // for a window size of chaincfg.Params.StakeDiffWindowSize. -func retrieveWindowBlocks(db *sql.DB, windowSize int64, limit, - offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { - rows, err := db.Query(internal.SelectWindowsByLimit, windowSize, limit, offset) +func retrieveWindowBlocks(ctx context.Context, db *sql.DB, windowSize int64, limit, offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { + rows, err := db.QueryContext(ctx, internal.SelectWindowsByLimit, windowSize, limit, offset) if err != nil { return nil, fmt.Errorf("retrieveWindowBlocks failed: error: %v", err) } @@ -620,9 +622,9 @@ func retrieveWindowBlocks(db *sql.DB, windowSize int64, limit, // retrieveTimeBasedBlockListing fetches blocks in chunks based on their block // time using the limit and offset provided. The time-based blocks groupings // include but are not limited to day, week, month and year. -func retrieveTimeBasedBlockListing(db *sql.DB, timeInterval string, limit, - offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { - rows, err := db.Query(internal.SelectBlocksTimeListingByLimit, timeInterval, +func retrieveTimeBasedBlockListing(ctx context.Context, db *sql.DB, timeInterval string, + limit, offset uint64) ([]*dbtypes.BlocksGroupedInfo, error) { + rows, err := db.QueryContext(ctx, internal.SelectBlocksTimeListingByLimit, timeInterval, limit, offset) if err != nil { return nil, fmt.Errorf("retrieveTimeBasedBlockListing failed: error: %v", err) @@ -659,9 +661,9 @@ func retrieveTimeBasedBlockListing(db *sql.DB, timeInterval string, limit, } // RetrieveUnspentTickets gets all unspent tickets. -func RetrieveUnspentTickets(db *sql.DB) (ids []uint64, hashes []string, err error) { +func RetrieveUnspentTickets(ctx context.Context, db *sql.DB) (ids []uint64, hashes []string, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectUnspentTickets) + rows, err = db.QueryContext(ctx, internal.SelectUnspentTickets) if err != nil { return ids, hashes, err } @@ -682,26 +684,28 @@ func RetrieveUnspentTickets(db *sql.DB) (ids []uint64, hashes []string, err erro return ids, hashes, err } -// RetrieveTicketIDByHash gets the db row ID (primary key) in the tickets table -// for the given ticket hash. -func RetrieveTicketIDByHash(db *sql.DB, ticketHash string) (id uint64, err error) { +// RetrieveTicketIDByHashNoCancel gets the db row ID (primary key) in the +// tickets table for the given ticket hash. As the name implies, this query +// should not accept a cancelable context. +func RetrieveTicketIDByHashNoCancel(db *sql.DB, ticketHash string) (id uint64, err error) { err = db.QueryRow(internal.SelectTicketIDByHash, ticketHash).Scan(&id) return } // RetrieveTicketStatusByHash gets the spend status and ticket pool status for // the given ticket hash. -func RetrieveTicketStatusByHash(db *sql.DB, ticketHash string) (id uint64, spendStatus dbtypes.TicketSpendType, - poolStatus dbtypes.TicketPoolStatus, err error) { - err = db.QueryRow(internal.SelectTicketStatusByHash, ticketHash).Scan(&id, &spendStatus, &poolStatus) +func RetrieveTicketStatusByHash(ctx context.Context, db *sql.DB, ticketHash string) (id uint64, + spendStatus dbtypes.TicketSpendType, poolStatus dbtypes.TicketPoolStatus, err error) { + err = db.QueryRowContext(ctx, internal.SelectTicketStatusByHash, ticketHash). + Scan(&id, &spendStatus, &poolStatus) return } // RetrieveTicketIDsByHashes gets the db row IDs (primary keys) in the tickets // table for the given ticket purchase transaction hashes. -func RetrieveTicketIDsByHashes(db *sql.DB, ticketHashes []string) (ids []uint64, err error) { +func RetrieveTicketIDsByHashes(ctx context.Context, db *sql.DB, ticketHashes []string) (ids []uint64, err error) { var dbtx *sql.Tx - dbtx, err = db.BeginTx(context.Background(), &sql.TxOptions{ + dbtx, err = db.BeginTx(ctx, &sql.TxOptions{ Isolation: sql.LevelDefault, ReadOnly: true, }) @@ -741,8 +745,8 @@ func RetrieveTicketIDsByHashes(db *sql.DB, ticketHashes []string) (ids []uint64, // purchase date. The maturity block is needed to identify immature tickets. // The grouping is done using the time-based group names provided e.g. months, // days, weeks and years. -func retrieveTicketsByDate(db *sql.DB, maturityBlock int64, groupBy string) (*dbtypes.PoolTicketsData, error) { - rows, err := db.Query(internal.MakeSelectTicketsByPurchaseDate(groupBy), maturityBlock) +func retrieveTicketsByDate(ctx context.Context, db *sql.DB, maturityBlock int64, groupBy string) (*dbtypes.PoolTicketsData, error) { + rows, err := db.QueryContext(ctx, internal.MakeSelectTicketsByPurchaseDate(groupBy), maturityBlock) if err != nil { return nil, err } @@ -775,9 +779,9 @@ func retrieveTicketsByDate(db *sql.DB, maturityBlock int64, groupBy string) (*db // purchase price. The maturity block is needed to identify immature tickets. // The grouping is done using the time-based group names provided e.g. months, // days, weeks and years. -func retrieveTicketByPrice(db *sql.DB, maturityBlock int64) (*dbtypes.PoolTicketsData, error) { +func retrieveTicketByPrice(ctx context.Context, db *sql.DB, maturityBlock int64) (*dbtypes.PoolTicketsData, error) { // Create the query statement and retrieve rows - rows, err := db.Query(internal.SelectTicketsByPrice, maturityBlock) + rows, err := db.QueryContext(ctx, internal.SelectTicketsByPrice, maturityBlock) if err != nil { return nil, err } @@ -804,9 +808,9 @@ func retrieveTicketByPrice(db *sql.DB, maturityBlock int64) (*dbtypes.PoolTicket // ticketpool grouped by ticket type (inferred by their output counts). The // grouping used here i.e. solo, pooled and tixsplit is just a guessing based on // commonly structured ticket purchases. -func retrieveTicketsGroupedByType(db *sql.DB) (*dbtypes.PoolTicketsData, error) { +func retrieveTicketsGroupedByType(ctx context.Context, db *sql.DB) (*dbtypes.PoolTicketsData, error) { var entry dbtypes.PoolTicketsData - rows, err := db.Query(internal.SelectTicketsByType) + rows, err := db.QueryContext(ctx, internal.SelectTicketsByType) if err != nil { return nil, err } @@ -833,9 +837,9 @@ func retrieveTicketsGroupedByType(db *sql.DB) (*dbtypes.PoolTicketsData, error) return &entry, nil } -func retrieveTicketSpendTypePerBlock(db *sql.DB) (*dbtypes.ChartsData, error) { +func retrieveTicketSpendTypePerBlock(ctx context.Context, db *sql.DB) (*dbtypes.ChartsData, error) { var items = new(dbtypes.ChartsData) - rows, err := db.Query(internal.SelectTicketSpendTypeByBlock) + rows, err := db.QueryContext(ctx, internal.SelectTicketSpendTypeByBlock) if err != nil { return nil, err } @@ -1062,35 +1066,31 @@ func InsertAddressRows(db *sql.DB, dbAs []*dbtypes.AddressRow, dupCheck, updateE return ids, dbtx.Commit() } -func RetrieveAddressRecvCount(db *sql.DB, address string) (count int64, err error) { - err = db.QueryRow(internal.SelectAddressRecvCount, address).Scan(&count) - return -} - -func RetrieveAddressUnspent(db *sql.DB, address string) (count, totalAmount int64, err error) { - err = db.QueryRow(internal.SelectAddressUnspentCountANDValue, address). +func RetrieveAddressUnspent(ctx context.Context, db *sql.DB, address string) (count, totalAmount int64, err error) { + err = db.QueryRowContext(ctx, internal.SelectAddressUnspentCountANDValue, address). Scan(&count, &totalAmount) return } -func RetrieveAddressSpent(db *sql.DB, address string) (count, totalAmount int64, err error) { - err = db.QueryRow(internal.SelectAddressSpentCountANDValue, address). +func RetrieveAddressSpent(ctx context.Context, db *sql.DB, address string) (count, totalAmount int64, err error) { + err = db.QueryRowContext(ctx, internal.SelectAddressSpentCountANDValue, address). Scan(&count, &totalAmount) return } -// retrieveAddressTxsCount return the number of record groups, where grouping -// is done by a specified time interval, for an address. -func retrieveAddressTxsCount(db *sql.DB, address, interval string) (count int64, err error) { - err = db.QueryRow(internal.MakeSelectAddressTimeGroupingCount(interval), address).Scan(&count) +// retrieveAddressTxsCount return the number of record groups, where grouping is +// done by a specified time interval, for an address. +func retrieveAddressTxsCount(ctx context.Context, db *sql.DB, address, interval string) (count int64, err error) { + err = db.QueryRowContext(ctx, internal.MakeSelectAddressTimeGroupingCount(interval), address).Scan(&count) return } // RetrieveAddressSpentUnspent gets the numbers of spent and unspent outpoints // for the given address, the total amounts spent and unspent, and the the // number of distinct spending transactions. -func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspent, +func RetrieveAddressSpentUnspent(ctx context.Context, db *sql.DB, address string) (numSpent, numUnspent, amtSpent, amtUnspent, numMergedSpent int64, err error) { + // The sql.Tx does not have a timeout, as the individial queries will. var dbtx *sql.Tx dbtx, err = db.BeginTx(context.Background(), &sql.TxOptions{ Isolation: sql.LevelDefault, @@ -1100,16 +1100,15 @@ func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspe err = fmt.Errorf("unable to begin database transaction: %v", err) return } - log.Debug("RetrieveAddressSpentUnspent", address) // Query for spent and unspent totals. var rows *sql.Rows - rows, err = db.Query(internal.SelectAddressSpentUnspentCountAndValue, address) + rows, err = db.QueryContext(ctx, internal.SelectAddressSpentUnspentCountAndValue, address) if err != nil && err != sql.ErrNoRows { if errRoll := dbtx.Rollback(); errRoll != nil { log.Errorf("Rollback failed: %v", errRoll) } - err = fmt.Errorf("unable to Query for spent and unspent amounts: %v", err) + err = fmt.Errorf("failed to query spent and unspent amounts: %v", err) return } if err == sql.ErrNoRows { @@ -1145,13 +1144,13 @@ func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspe // Query for spending transaction count, repeated transaction hashes merged. var nms sql.NullInt64 - err = dbtx.QueryRow(internal.SelectAddressesMergedSpentCount, address). + err = dbtx.QueryRowContext(ctx, internal.SelectAddressesMergedSpentCount, address). Scan(&nms) if err != nil && err != sql.ErrNoRows { if errRoll := dbtx.Rollback(); errRoll != nil { log.Errorf("Rollback failed: %v", errRoll) } - err = fmt.Errorf("unable to QueryRow for merged spent count: %v", err) + err = fmt.Errorf("failed to query merged spent count: %v", err) return } @@ -1165,15 +1164,16 @@ func RetrieveAddressSpentUnspent(db *sql.DB, address string) (numSpent, numUnspe } // RetrieveAddressUTXOs gets the unspent transaction outputs (UTXOs) paying to -// the specified address. -func RetrieveAddressUTXOs(db *sql.DB, address string, currentBlockHeight int64) ([]apitypes.AddressTxnOutput, error) { +// the specified address. The input current block height is used to compute +// confirmations of the located transactions. +func RetrieveAddressUTXOs(ctx context.Context, db *sql.DB, address string, currentBlockHeight int64) ([]apitypes.AddressTxnOutput, error) { stmt, err := db.Prepare(internal.SelectAddressUnspentWithTxn) if err != nil { log.Error(err) return nil, err } - rows, err := stmt.Query(address) + rows, err := stmt.QueryContext(ctx, address) // _ = stmt.Close() // or does Rows.Close() do it? if err != nil { log.Error(err) @@ -1206,28 +1206,27 @@ func RetrieveAddressUTXOs(db *sql.DB, address string, currentBlockHeight int64) // and return them sorted by time in descending order. It will also return a // short list of recently (defined as greater than recentBlockHeight) confirmed // transactions that can be used to validate mempool status. -func RetrieveAddressTxnsOrdered(db *sql.DB, addresses []string, recentBlockHeight int64) (txs []string, recenttxs []string) { +func RetrieveAddressTxnsOrdered(ctx context.Context, db *sql.DB, addresses []string, recentBlockHeight int64) (txs []string, recenttxs []string, err error) { var txHash string var height int64 - stmt, err := db.Prepare(internal.SelectAddressesAllTxn) + var stmt *sql.Stmt + stmt, err = db.Prepare(internal.SelectAddressesAllTxn) if err != nil { - log.Error(err) - return nil, nil + return nil, nil, err } - rows, err := stmt.Query(pq.Array(addresses)) + var rows *sql.Rows + rows, err = stmt.QueryContext(ctx, pq.Array(addresses)) // _ = stmt.Close() // or does Rows.Close do it? if err != nil { - log.Error(err) - return nil, nil + return nil, nil, err } defer closeRows(rows) for rows.Next() { err = rows.Scan(&txHash, &height) if err != nil { - log.Error(err) - return + return // return what we got, plus the error } txs = append(txs, txHash) if height > recentBlockHeight { @@ -1237,8 +1236,10 @@ func RetrieveAddressTxnsOrdered(db *sql.DB, addresses []string, recentBlockHeigh return } -func RetrieveAllAddressTxns(db *sql.DB, address string) ([]uint64, []*dbtypes.AddressRow, error) { - rows, err := db.Query(internal.SelectAddressAllByAddress, address) +// RetrieveAllAddressTxns retrieves all rows of the address table pertaining to +// the given address. +func RetrieveAllAddressTxns(ctx context.Context, db *sql.DB, address string) ([]uint64, []*dbtypes.AddressRow, error) { + rows, err := db.QueryContext(ctx, internal.SelectAddressAllByAddress, address) if err != nil { return nil, nil, err } @@ -1248,29 +1249,29 @@ func RetrieveAllAddressTxns(db *sql.DB, address string) ([]uint64, []*dbtypes.Ad return scanAddressQueryRows(rows) } -func RetrieveAddressTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { - return retrieveAddressTxns(db, address, N, offset, +func RetrieveAddressTxns(ctx context.Context, db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { + return retrieveAddressTxns(ctx, db, address, N, offset, internal.SelectAddressLimitNByAddress, false) } -func RetrieveAddressDebitTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { - return retrieveAddressTxns(db, address, N, offset, +func RetrieveAddressDebitTxns(ctx context.Context, db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { + return retrieveAddressTxns(ctx, db, address, N, offset, internal.SelectAddressDebitsLimitNByAddress, false) } -func RetrieveAddressCreditTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { - return retrieveAddressTxns(db, address, N, offset, +func RetrieveAddressCreditTxns(ctx context.Context, db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { + return retrieveAddressTxns(ctx, db, address, N, offset, internal.SelectAddressCreditsLimitNByAddress, false) } -func RetrieveAddressMergedDebitTxns(db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { - return retrieveAddressTxns(db, address, N, offset, +func RetrieveAddressMergedDebitTxns(ctx context.Context, db *sql.DB, address string, N, offset int64) ([]uint64, []*dbtypes.AddressRow, error) { + return retrieveAddressTxns(ctx, db, address, N, offset, internal.SelectAddressMergedDebitView, true) } -func retrieveAddressTxns(db *sql.DB, address string, N, offset int64, +func retrieveAddressTxns(ctx context.Context, db *sql.DB, address string, N, offset int64, statement string, isMergedDebitView bool) ([]uint64, []*dbtypes.AddressRow, error) { - rows, err := db.Query(statement, address, N, offset) + rows, err := db.QueryContext(ctx, statement, address, N, offset) if err != nil { return nil, nil, err } @@ -1336,12 +1337,11 @@ func scanAddressQueryRows(rows *sql.Rows) (ids []uint64, addressRows []*dbtypes. // RetrieveAddressIDsByOutpoint fetches all address row IDs for a given outpoint // (hash:index). // Update Vin due to DCRD AMOUNTIN - START - DO NOT MERGE CHANGES IF DCRD FIXED -func RetrieveAddressIDsByOutpoint(db *sql.DB, txHash string, - voutIndex uint32) ([]uint64, []string, int64, error) { +func RetrieveAddressIDsByOutpoint(ctx context.Context, db *sql.DB, txHash string, voutIndex uint32) ([]uint64, []string, int64, error) { var ids []uint64 var addresses []string var value int64 - rows, err := db.Query(internal.SelectAddressIDsByFundingOutpoint, txHash, voutIndex) + rows, err := db.QueryContext(ctx, internal.SelectAddressIDsByFundingOutpoint, txHash, voutIndex) if err != nil { return ids, addresses, 0, err } @@ -1365,8 +1365,8 @@ func RetrieveAddressIDsByOutpoint(db *sql.DB, txHash string, // retrieveOldestTxBlockTime helps choose the most appropriate address page // graph grouping to load by default depending on when the first transaction to // the specific address was made. -func retrieveOldestTxBlockTime(db *sql.DB, addr string) (blockTime dbtypes.TimeDef, err error) { - err = db.QueryRow(internal.SelectAddressOldestTxBlockTime, addr).Scan(&blockTime.T) +func retrieveOldestTxBlockTime(ctx context.Context, db *sql.DB, addr string) (blockTime dbtypes.TimeDef, err error) { + err = db.QueryRowContext(ctx, internal.SelectAddressOldestTxBlockTime, addr).Scan(&blockTime.T) return } @@ -1375,11 +1375,8 @@ func retrieveOldestTxBlockTime(db *sql.DB, addr string) (blockTime dbtypes.TimeD // The time interval is grouping records by week, month, year, day and all. // For all time interval, transactions are grouped by the unique // timestamps (blocks) available. -func retrieveTxHistoryByType(db *sql.DB, addr, - timeInterval string) (*dbtypes.ChartsData, error) { - var items = new(dbtypes.ChartsData) - - rows, err := db.Query(internal.MakeSelectAddressTxTypesByAddress(timeInterval), +func retrieveTxHistoryByType(ctx context.Context, db *sql.DB, addr, timeInterval string) (*dbtypes.ChartsData, error) { + rows, err := db.QueryContext(ctx, internal.MakeSelectAddressTxTypesByAddress(timeInterval), addr) if err != nil { return nil, err @@ -1387,6 +1384,7 @@ func retrieveTxHistoryByType(db *sql.DB, addr, defer closeRows(rows) + items := new(dbtypes.ChartsData) for rows.Next() { var blockTime dbtypes.TimeDef var sentRtx, receivedRtx, tickets, votes, revokeTx uint64 @@ -1410,11 +1408,10 @@ func retrieveTxHistoryByType(db *sql.DB, addr, // the given time interval. The time interval is grouping records by week, // month, year, day and all. For all time interval, transactions are grouped by // the unique timestamps (blocks) available. -func retrieveTxHistoryByAmountFlow(db *sql.DB, addr, - timeInterval string) (*dbtypes.ChartsData, error) { +func retrieveTxHistoryByAmountFlow(ctx context.Context, db *sql.DB, addr, timeInterval string) (*dbtypes.ChartsData, error) { var items = new(dbtypes.ChartsData) - rows, err := db.Query(internal.MakeSelectAddressAmountFlowByAddress(timeInterval), addr) + rows, err := db.QueryContext(ctx, internal.MakeSelectAddressAmountFlowByAddress(timeInterval), addr) if err != nil { return nil, err } @@ -1446,12 +1443,11 @@ func retrieveTxHistoryByAmountFlow(db *sql.DB, addr, // The time interval is grouping records by week, month, year, day and all. // For all time interval, transactions are grouped by the unique // timestamps (blocks) available. -func retrieveTxHistoryByUnspentAmount(db *sql.DB, addr, - timeInterval string) (*dbtypes.ChartsData, error) { +func retrieveTxHistoryByUnspentAmount(ctx context.Context, db *sql.DB, addr, timeInterval string) (*dbtypes.ChartsData, error) { var totalAmount uint64 var items = new(dbtypes.ChartsData) - rows, err := db.Query(internal.MakeSelectAddressUnspentAmountByAddress(timeInterval), addr) + rows, err := db.QueryContext(ctx, internal.MakeSelectAddressUnspentAmountByAddress(timeInterval), addr) if err != nil { return nil, err } @@ -1624,29 +1620,34 @@ func InsertVouts(db *sql.DB, dbVouts []*dbtypes.Vout, checked bool, updateOnConf return ids, addressRows, dbtx.Commit() } -func RetrievePkScriptByID(db *sql.DB, id uint64) (pkScript []byte, ver uint16, err error) { - err = db.QueryRow(internal.SelectPkScriptByID, id).Scan(&ver, &pkScript) +func RetrievePkScriptByVinID(ctx context.Context, db *sql.DB, vinID uint64) (pkScript []byte, ver uint16, err error) { + err = db.QueryRowContext(ctx, internal.SelectPkScriptByVinID, vinID).Scan(&ver, &pkScript) return } -func RetrievePkScriptByOutpoint(db *sql.DB, txHash string, voutIndex uint32) (pkScript []byte, ver uint16, err error) { - err = db.QueryRow(internal.SelectPkScriptByOutpoint, txHash, voutIndex).Scan(&ver, &pkScript) +func RetrievePkScriptByVoutID(ctx context.Context, db *sql.DB, voutID uint64) (pkScript []byte, ver uint16, err error) { + err = db.QueryRowContext(ctx, internal.SelectPkScriptByID, voutID).Scan(&ver, &pkScript) return } -func RetrieveVoutIDByOutpoint(db *sql.DB, txHash string, voutIndex uint32) (id uint64, err error) { - err = db.QueryRow(internal.SelectVoutIDByOutpoint, txHash, voutIndex).Scan(&id) +func RetrievePkScriptByOutpoint(ctx context.Context, db *sql.DB, txHash string, voutIndex uint32) (pkScript []byte, ver uint16, err error) { + err = db.QueryRowContext(ctx, internal.SelectPkScriptByOutpoint, txHash, voutIndex).Scan(&ver, &pkScript) return } -func RetrieveVoutValue(db *sql.DB, txHash string, voutIndex uint32) (value uint64, err error) { - err = db.QueryRow(internal.RetrieveVoutValue, txHash, voutIndex).Scan(&value) +func RetrieveVoutIDByOutpoint(ctx context.Context, db *sql.DB, txHash string, voutIndex uint32) (id uint64, err error) { + err = db.QueryRowContext(ctx, internal.SelectVoutIDByOutpoint, txHash, voutIndex).Scan(&id) return } -func RetrieveVoutValues(db *sql.DB, txHash string) (values []uint64, txInds []uint32, txTrees []int8, err error) { +func RetrieveVoutValue(ctx context.Context, db *sql.DB, txHash string, voutIndex uint32) (value uint64, err error) { + err = db.QueryRowContext(ctx, internal.RetrieveVoutValue, txHash, voutIndex).Scan(&value) + return +} + +func RetrieveVoutValues(ctx context.Context, db *sql.DB, txHash string) (values []uint64, txInds []uint32, txTrees []int8, err error) { var rows *sql.Rows - rows, err = db.Query(internal.RetrieveVoutValues, txHash) + rows, err = db.QueryContext(ctx, internal.RetrieveVoutValues, txHash) if err != nil { return } @@ -1670,6 +1671,8 @@ func RetrieveVoutValues(db *sql.DB, txHash string) (values []uint64, txInds []ui } // RetrieveAllVinDbIDs gets every row ID (the primary keys) for the vins table. +// This function is used in UpdateSpendingInfoInAllAddresses, so it should not +// be subject to timeouts. func RetrieveAllVinDbIDs(db *sql.DB) (vinDbIDs []uint64, err error) { var rows *sql.Rows rows, err = db.Query(internal.SelectVinIDsALL) @@ -1693,17 +1696,17 @@ func RetrieveAllVinDbIDs(db *sql.DB) (vinDbIDs []uint64, err error) { // RetrieveFundingOutpointByTxIn gets the previous outpoint for a transaction // input specified by transaction hash and input index. -func RetrieveFundingOutpointByTxIn(db *sql.DB, txHash string, +func RetrieveFundingOutpointByTxIn(ctx context.Context, db *sql.DB, txHash string, vinIndex uint32) (id uint64, tx string, index uint32, tree int8, err error) { - err = db.QueryRow(internal.SelectFundingOutpointByTxIn, txHash, vinIndex). + err = db.QueryRowContext(ctx, internal.SelectFundingOutpointByTxIn, txHash, vinIndex). Scan(&id, &tx, &index, &tree) return } // RetrieveFundingOutpointByVinID gets the previous outpoint for a transaction // input specified by row ID in the vins table. -func RetrieveFundingOutpointByVinID(db *sql.DB, vinDbID uint64) (tx string, index uint32, tree int8, err error) { - err = db.QueryRow(internal.SelectFundingOutpointByVinID, vinDbID). +func RetrieveFundingOutpointByVinID(ctx context.Context, db *sql.DB, vinDbID uint64) (tx string, index uint32, tree int8, err error) { + err = db.QueryRowContext(ctx, internal.SelectFundingOutpointByVinID, vinDbID). Scan(&tx, &index, &tree) return } @@ -1711,23 +1714,25 @@ func RetrieveFundingOutpointByVinID(db *sql.DB, vinDbID uint64) (tx string, inde // RetrieveFundingOutpointIndxByVinID gets the transaction output index of the // previous outpoint for a transaction input specified by row ID in the vins // table. -func RetrieveFundingOutpointIndxByVinID(db *sql.DB, vinDbID uint64) (idx uint32, err error) { - err = db.QueryRow(internal.SelectFundingOutpointIndxByVinID, vinDbID).Scan(&idx) +func RetrieveFundingOutpointIndxByVinID(ctx context.Context, db *sql.DB, vinDbID uint64) (idx uint32, err error) { + err = db.QueryRowContext(ctx, internal.SelectFundingOutpointIndxByVinID, vinDbID).Scan(&idx) return } // RetrieveFundingTxByTxIn gets the transaction hash of the previous outpoint // for a transaction input specified by hash and input index. -func RetrieveFundingTxByTxIn(db *sql.DB, txHash string, vinIndex uint32) (id uint64, tx string, err error) { - err = db.QueryRow(internal.SelectFundingTxByTxIn, txHash, vinIndex). +func RetrieveFundingTxByTxIn(ctx context.Context, db *sql.DB, txHash string, vinIndex uint32) (id uint64, tx string, err error) { + err = db.QueryRowContext(ctx, internal.SelectFundingTxByTxIn, txHash, vinIndex). Scan(&id, &tx) return } // RetrieveFundingTxByVinDbID gets the transaction hash of the previous outpoint -// for a transaction input specified by row ID in the vins table. -func RetrieveFundingTxByVinDbID(db *sql.DB, vinDbID uint64) (tx string, err error) { - err = db.QueryRow(internal.SelectFundingTxByVinID, vinDbID).Scan(&tx) +// for a transaction input specified by row ID in the vins table. This function +// is used only in UpdateSpendingInfoInAllTickets, so it should not be subject +// to timeouts. +func RetrieveFundingTxByVinDbID(ctx context.Context, db *sql.DB, vinDbID uint64) (tx string, err error) { + err = db.QueryRowContext(ctx, internal.SelectFundingTxByVinID, vinDbID).Scan(&tx) return } @@ -1759,27 +1764,32 @@ func RetrieveFundingTxsByTx(db *sql.DB, txHash string) ([]uint64, []*dbtypes.Tx, // RetrieveSpendingTxByVinID gets the spending transaction input (hash, vin // number, and tx tree) for the transaction input specified by row ID in the // vins table. -func RetrieveSpendingTxByVinID(db *sql.DB, vinDbID uint64) (tx string, +func RetrieveSpendingTxByVinID(ctx context.Context, db *sql.DB, vinDbID uint64) (tx string, vinIndex uint32, tree int8, err error) { - err = db.QueryRow(internal.SelectSpendingTxByVinID, vinDbID).Scan(&tx, &vinIndex, &tree) + err = db.QueryRowContext(ctx, internal.SelectSpendingTxByVinID, vinDbID). + Scan(&tx, &vinIndex, &tree) return } // RetrieveSpendingTxByTxOut gets any spending transaction input info for a -// previous outpoint specified by funding transaction hash and vout number. -func RetrieveSpendingTxByTxOut(db *sql.DB, txHash string, +// previous outpoint specified by funding transaction hash and vout number. This +// function is called by SpendingTransaction, an important part of the address +// page loading. +func RetrieveSpendingTxByTxOut(ctx context.Context, db *sql.DB, txHash string, voutIndex uint32) (id uint64, tx string, vin uint32, tree int8, err error) { - err = db.QueryRow(internal.SelectSpendingTxByPrevOut, + err = db.QueryRowContext(ctx, internal.SelectSpendingTxByPrevOut, txHash, voutIndex).Scan(&id, &tx, &vin, &tree) return } // RetrieveSpendingTxsByFundingTx gets info on all spending transaction inputs -// for the given funding transaction specified by DB row ID. -func RetrieveSpendingTxsByFundingTx(db *sql.DB, fundingTxID string) (dbIDs []uint64, +// for the given funding transaction specified by DB row ID. This function is +// called by SpendingTransactions, an important part of the transaction page +// loading, among other functions.. +func RetrieveSpendingTxsByFundingTx(ctx context.Context, db *sql.DB, fundingTxID string) (dbIDs []uint64, txns []string, vinInds []uint32, voutInds []uint32, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectSpendingTxsByPrevTx, fundingTxID) + rows, err = db.QueryContext(ctx, internal.SelectSpendingTxsByPrevTx, fundingTxID) if err != nil { return } @@ -1804,11 +1814,11 @@ func RetrieveSpendingTxsByFundingTx(db *sql.DB, fundingTxID string) (dbIDs []uin } // RetrieveSpendingTxsByFundingTxWithBlockHeight will retrieve all transactions, -// indexes and block heights funded by a specific transaction. -func RetrieveSpendingTxsByFundingTxWithBlockHeight(db *sql.DB, - fundingTxID string) (aSpendByFunHash []*apitypes.SpendByFundingHash, err error) { +// indexes and block heights funded by a specific transaction. This function is +// used by the DCR to Insight transaction converter. +func RetrieveSpendingTxsByFundingTxWithBlockHeight(ctx context.Context, db *sql.DB, fundingTxID string) (aSpendByFunHash []*apitypes.SpendByFundingHash, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectSpendingTxsByPrevTxWithBlockHeight, fundingTxID) + rows, err = db.QueryContext(ctx, internal.SelectSpendingTxsByPrevTxWithBlockHeight, fundingTxID) if err != nil { return } @@ -1827,22 +1837,26 @@ func RetrieveSpendingTxsByFundingTxWithBlockHeight(db *sql.DB, return } -func RetrieveVinByID(db *sql.DB, vinDbID uint64) (prevOutHash string, prevOutVoutInd uint32, +// RetrieveVinByID gets from the vins table for the provided row ID. +func RetrieveVinByID(ctx context.Context, db *sql.DB, vinDbID uint64) (prevOutHash string, prevOutVoutInd uint32, prevOutTree int8, txHash string, txVinInd uint32, txTree int8, valueIn int64, err error) { var blockTime dbtypes.TimeDef var isValid, isMainchain bool var txType uint32 - err = db.QueryRow(internal.SelectAllVinInfoByID, vinDbID). + err = db.QueryRowContext(ctx, internal.SelectAllVinInfoByID, vinDbID). Scan(&txHash, &txVinInd, &txTree, &isValid, &isMainchain, &blockTime.T, &prevOutHash, &prevOutVoutInd, &prevOutTree, &valueIn, &txType) return } -func RetrieveVinsByIDs(db *sql.DB, vinDbIDs []uint64) ([]dbtypes.VinTxProperty, error) { +// RetrieveVinsByIDs retrieves vin details for the rows of the vins table +// specified by the provided row IDs. This function is an important part of the +// transaction page. +func RetrieveVinsByIDs(ctx context.Context, db *sql.DB, vinDbIDs []uint64) ([]dbtypes.VinTxProperty, error) { vins := make([]dbtypes.VinTxProperty, len(vinDbIDs)) for i, id := range vinDbIDs { vin := &vins[i] - err := db.QueryRow(internal.SelectAllVinInfoByID, id).Scan(&vin.TxID, + err := db.QueryRowContext(ctx, internal.SelectAllVinInfoByID, id).Scan(&vin.TxID, &vin.TxIndex, &vin.TxTree, &vin.IsValid, &vin.IsMainchain, &vin.Time.T, &vin.PrevTxHash, &vin.PrevTxIndex, &vin.PrevTxTree, &vin.ValueIn, &vin.TxType) @@ -1853,14 +1867,17 @@ func RetrieveVinsByIDs(db *sql.DB, vinDbIDs []uint64) ([]dbtypes.VinTxProperty, return vins, nil } -func RetrieveVoutsByIDs(db *sql.DB, voutDbIDs []uint64) ([]dbtypes.Vout, error) { +// RetrieveVoutsByIDs retrieves vout details for the rows of the vouts table +// specified by the provided row IDs. This function is an important part of the +// transaction page. +func RetrieveVoutsByIDs(ctx context.Context, db *sql.DB, voutDbIDs []uint64) ([]dbtypes.Vout, error) { vouts := make([]dbtypes.Vout, len(voutDbIDs)) for i, id := range voutDbIDs { vout := &vouts[i] var id0 uint64 var reqSigs uint32 var scriptType, addresses string - err := db.QueryRow(internal.SelectVoutByID, id).Scan(&id0, &vout.TxHash, + err := db.QueryRowContext(ctx, internal.SelectVoutByID, id).Scan(&id0, &vout.TxHash, &vout.TxIndex, &vout.TxTree, &vout.Value, &vout.Version, &vout.ScriptPubKey, &reqSigs, &scriptType, &addresses) if err != nil { @@ -2097,8 +2114,8 @@ func insertSpendingAddressRow(tx *sql.Tx, fundingTxHash string, fundingTxVoutInd } // retrieveCoinSupply fetches the coin supply data from the vins table. -func retrieveCoinSupply(db *sql.DB) (*dbtypes.ChartsData, error) { - rows, err := db.Query(internal.SelectCoinSupply) +func retrieveCoinSupply(ctx context.Context, db *sql.DB) (*dbtypes.ChartsData, error) { + rows, err := db.QueryContext(ctx, internal.SelectCoinSupply) if err != nil { return nil, err } @@ -2134,14 +2151,14 @@ func retrieveCoinSupply(db *sql.DB) (*dbtypes.ChartsData, error) { // accumulate over time (cumulative sum), whereas for block intervals the counts // are just for the block. The total length of time over all intervals always // spans the locked-in period of the agenda. -func retrieveAgendaVoteChoices(db *sql.DB, agendaID string, byType int) (*dbtypes.AgendaVoteChoices, error) { +func retrieveAgendaVoteChoices(ctx context.Context, db *sql.DB, agendaID string, byType int) (*dbtypes.AgendaVoteChoices, error) { // Query with block or day interval size var query = internal.SelectAgendasAgendaVotesByTime if byType == 1 { query = internal.SelectAgendasAgendaVotesByHeight } - rows, err := db.Query(query, dbtypes.Yes, dbtypes.Abstain, dbtypes.No, + rows, err := db.QueryContext(ctx, query, dbtypes.Yes, dbtypes.Abstain, dbtypes.No, agendaID) if err != nil { return nil, err @@ -2244,11 +2261,15 @@ func InsertTxns(db *sql.DB, dbTxns []*dbtypes.Tx, checked, updateExistingRecords return ids, dbtx.Commit() } -func RetrieveDbTxByHash(db *sql.DB, txHash string) (id uint64, dbTx *dbtypes.Tx, err error) { +// RetrieveDbTxByHash retrieves a row of the transactions table corresponding to +// the given transaction hash. Transactions in valid and mainchain blocks are +// chosen first. This function is used by FillAddressTransactions, an important +// component of the addresses page. +func RetrieveDbTxByHash(ctx context.Context, db *sql.DB, txHash string) (id uint64, dbTx *dbtypes.Tx, err error) { dbTx = new(dbtypes.Tx) vinDbIDs := dbtypes.UInt64Array(dbTx.VinDbIds) voutDbIDs := dbtypes.UInt64Array(dbTx.VoutDbIds) - err = db.QueryRow(internal.SelectFullTxByHash, txHash).Scan(&id, + err = db.QueryRowContext(ctx, internal.SelectFullTxByHash, txHash).Scan(&id, &dbTx.BlockHash, &dbTx.BlockHeight, &dbTx.BlockTime.T, &dbTx.Time.T, &dbTx.TxType, &dbTx.Version, &dbTx.Tree, &dbTx.TxID, &dbTx.BlockIndex, &dbTx.Locktime, &dbTx.Expiry, &dbTx.Size, &dbTx.Spent, &dbTx.Sent, @@ -2259,14 +2280,17 @@ func RetrieveDbTxByHash(db *sql.DB, txHash string) (id uint64, dbTx *dbtypes.Tx, return } -func RetrieveFullTxByHash(db *sql.DB, txHash string) (id uint64, +// RetrieveFullTxByHash gets all data from the transactions table for the +// transaction specified by its hash. Transactions in valid and mainchain blocks +// are chosen first. See also RetrieveDbTxByHash. +func RetrieveFullTxByHash(ctx context.Context, db *sql.DB, txHash string) (id uint64, blockHash string, blockHeight int64, blockTime, timeVal dbtypes.TimeDef, txType int16, version int32, tree int8, blockInd uint32, lockTime, expiry int32, size uint32, spent, sent, fees int64, numVin int32, vinDbIDs []int64, numVout int32, voutDbIDs []int64, isValidBlock, isMainchainBlock bool, err error) { var hash string - err = db.QueryRow(internal.SelectFullTxByHash, txHash).Scan(&id, &blockHash, + err = db.QueryRowContext(ctx, internal.SelectFullTxByHash, txHash).Scan(&id, &blockHash, &blockHeight, &blockTime.T, &timeVal.T, &txType, &version, &tree, &hash, &blockInd, &lockTime, &expiry, &size, &spent, &sent, &fees, &numVin, &vinDbIDs, &numVout, &voutDbIDs, @@ -2275,10 +2299,11 @@ func RetrieveFullTxByHash(db *sql.DB, txHash string) (id uint64, } // RetrieveDbTxsByHash retrieves all the rows of the transactions table, -// including the primary keys/ids, for the given transaction hash. -func RetrieveDbTxsByHash(db *sql.DB, txHash string) (ids []uint64, dbTxs []*dbtypes.Tx, err error) { +// including the primary keys/ids, for the given transaction hash. This function +// is used by the transaction page via ChainDB.Transaction. +func RetrieveDbTxsByHash(ctx context.Context, db *sql.DB, txHash string) (ids []uint64, dbTxs []*dbtypes.Tx, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectFullTxsByHash, txHash) + rows, err = db.QueryContext(ctx, internal.SelectFullTxsByHash, txHash) if err != nil { return } @@ -2311,11 +2336,13 @@ func RetrieveDbTxsByHash(db *sql.DB, txHash string) (ids []uint64, dbTxs []*dbty } // RetrieveTxnsVinsByBlock retrieves for all the transactions in the specified -// block the vin_db_ids arrays, is_valid, and is_mainchain. -func RetrieveTxnsVinsByBlock(db *sql.DB, blockHash string) (vinDbIDs []dbtypes.UInt64Array, +// block the vin_db_ids arrays, is_valid, and is_mainchain. This function is +// used by handleVinsTableMainchainupgrade, so it should not be subject to +// timeouts. +func RetrieveTxnsVinsByBlock(ctx context.Context, db *sql.DB, blockHash string) (vinDbIDs []dbtypes.UInt64Array, areValid []bool, areMainchain []bool, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectTxnsVinsByBlock, blockHash) + rows, err = db.QueryContext(ctx, internal.SelectTxnsVinsByBlock, blockHash) if err != nil { return } @@ -2337,8 +2364,10 @@ func RetrieveTxnsVinsByBlock(db *sql.DB, blockHash string) (vinDbIDs []dbtypes.U } // RetrieveTxnsVinsVoutsByBlock retrieves for all the transactions in the -// specified block the vin_db_ids and vout_db_ids arrays. -func RetrieveTxnsVinsVoutsByBlock(db *sql.DB, blockHash string, onlyRegular bool) (vinDbIDs, voutDbIDs []dbtypes.UInt64Array, +// specified block the vin_db_ids and vout_db_ids arrays. This function is used +// only by UpdateLastAddressesValid and other setting functions, where it should +// not be subject to a timeout. +func RetrieveTxnsVinsVoutsByBlock(ctx context.Context, db *sql.DB, blockHash string, onlyRegular bool) (vinDbIDs, voutDbIDs []dbtypes.UInt64Array, areMainchain []bool, err error) { stmt := internal.SelectTxnsVinsVoutsByBlock if onlyRegular { @@ -2346,7 +2375,7 @@ func RetrieveTxnsVinsVoutsByBlock(db *sql.DB, blockHash string, onlyRegular bool } var rows *sql.Rows - rows, err = db.Query(stmt, blockHash) + rows, err = db.QueryContext(ctx, stmt, blockHash) if err != nil { return } @@ -2367,21 +2396,23 @@ func RetrieveTxnsVinsVoutsByBlock(db *sql.DB, blockHash string, onlyRegular bool return } -func RetrieveTxByHash(db *sql.DB, txHash string) (id uint64, blockHash string, +func RetrieveTxByHash(ctx context.Context, db *sql.DB, txHash string) (id uint64, blockHash string, blockInd uint32, tree int8, err error) { - err = db.QueryRow(internal.SelectTxByHash, txHash).Scan(&id, &blockHash, &blockInd, &tree) + err = db.QueryRowContext(ctx, internal.SelectTxByHash, txHash).Scan(&id, &blockHash, &blockInd, &tree) return } -func RetrieveTxBlockTimeByHash(db *sql.DB, txHash string) (blockTime dbtypes.TimeDef, err error) { - err = db.QueryRow(internal.SelectTxBlockTimeByHash, txHash).Scan(&blockTime.T) +func RetrieveTxBlockTimeByHash(ctx context.Context, db *sql.DB, txHash string) (blockTime dbtypes.TimeDef, err error) { + err = db.QueryRowContext(ctx, internal.SelectTxBlockTimeByHash, txHash).Scan(&blockTime.T) return } -func RetrieveTxsByBlockHash(db *sql.DB, blockHash string) (ids []uint64, txs []string, +// This is used by update functions, so care should be taken to not timeout in +// these cases. +func RetrieveTxsByBlockHash(ctx context.Context, db *sql.DB, blockHash string) (ids []uint64, txs []string, blockInds []uint32, trees []int8, blockTimes []dbtypes.TimeDef, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectTxsByBlockHash, blockHash) + rows, err = db.QueryContext(ctx, internal.SelectTxsByBlockHash, blockHash) if err != nil { return } @@ -2411,9 +2442,9 @@ func RetrieveTxsByBlockHash(db *sql.DB, blockHash string) (ids []uint64, txs []s // RetrieveTxnsBlocks retrieves for the specified transaction hash the following // data for each block containing the transactions: block_hash, block_index, // is_valid, is_mainchain. -func RetrieveTxnsBlocks(db *sql.DB, txHash string) (blockHashes []string, blockHeights, blockIndexes []uint32, areValid, areMainchain []bool, err error) { +func RetrieveTxnsBlocks(ctx context.Context, db *sql.DB, txHash string) (blockHashes []string, blockHeights, blockIndexes []uint32, areValid, areMainchain []bool, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectTxsBlocks, txHash) + rows, err = db.QueryContext(ctx, internal.SelectTxsBlocks, txHash) if err != nil { return } @@ -2437,8 +2468,8 @@ func RetrieveTxnsBlocks(db *sql.DB, txHash string) (blockHashes []string, blockH return } -func retrieveTxPerDay(db *sql.DB) (*dbtypes.ChartsData, error) { - rows, err := db.Query(internal.SelectTxsPerDay) +func retrieveTxPerDay(ctx context.Context, db *sql.DB) (*dbtypes.ChartsData, error) { + rows, err := db.QueryContext(ctx, internal.SelectTxsPerDay) if err != nil { return nil, err } @@ -2459,7 +2490,7 @@ func retrieveTxPerDay(db *sql.DB) (*dbtypes.ChartsData, error) { return items, nil } -func retrieveTicketByOutputCount(db *sql.DB, dataType outputCountType) (*dbtypes.ChartsData, error) { +func retrieveTicketByOutputCount(ctx context.Context, db *sql.DB, dataType outputCountType) (*dbtypes.ChartsData, error) { var query string switch dataType { case outputCountByAllBlocks: @@ -2470,7 +2501,7 @@ func retrieveTicketByOutputCount(db *sql.DB, dataType outputCountType) (*dbtypes return nil, fmt.Errorf("unknown output count type '%v'", dataType) } - rows, err := db.Query(query) + rows, err := db.QueryContext(ctx, query) if err != nil { return nil, err } @@ -2519,43 +2550,43 @@ func InsertBlockPrevNext(db *sql.DB, blockDbID uint64, } // RetrieveBestBlockHeight gets the best block height (main chain only). -func RetrieveBestBlockHeight(db *sql.DB) (height uint64, hash string, id uint64, err error) { - err = db.QueryRow(internal.RetrieveBestBlockHeight).Scan(&id, &hash, &height) +func RetrieveBestBlockHeight(ctx context.Context, db *sql.DB) (height uint64, hash string, id uint64, err error) { + err = db.QueryRowContext(ctx, internal.RetrieveBestBlockHeight).Scan(&id, &hash, &height) return } // RetrieveBestBlockHeightAny gets the best block height, including side chains. -func RetrieveBestBlockHeightAny(db *sql.DB) (height uint64, hash string, id uint64, err error) { - err = db.QueryRow(internal.RetrieveBestBlockHeightAny).Scan(&id, &hash, &height) +func RetrieveBestBlockHeightAny(ctx context.Context, db *sql.DB) (height uint64, hash string, id uint64, err error) { + err = db.QueryRowContext(ctx, internal.RetrieveBestBlockHeightAny).Scan(&id, &hash, &height) return } // RetrieveBlockHash retrieves the hash of the block at the given height, if it // exists (be sure to check error against sql.ErrNoRows!). WARNING: this returns // the most recently added block at this height, but there may be others. -func RetrieveBlockHash(db *sql.DB, idx int64) (hash string, err error) { - err = db.QueryRow(internal.SelectBlockHashByHeight, idx).Scan(&hash) +func RetrieveBlockHash(ctx context.Context, db *sql.DB, idx int64) (hash string, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockHashByHeight, idx).Scan(&hash) return } // RetrieveBlockHeight retrieves the height of the block with the given hash, if // it exists (be sure to check error against sql.ErrNoRows!). -func RetrieveBlockHeight(db *sql.DB, hash string) (height int64, err error) { - err = db.QueryRow(internal.SelectBlockHeightByHash, hash).Scan(&height) +func RetrieveBlockHeight(ctx context.Context, db *sql.DB, hash string) (height int64, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockHeightByHash, hash).Scan(&height) return } // RetrieveBlockVoteCount gets the number of votes mined in a block. -func RetrieveBlockVoteCount(db *sql.DB, hash string) (numVotes int16, err error) { - err = db.QueryRow(internal.SelectBlockVoteCount, hash).Scan(&numVotes) +func RetrieveBlockVoteCount(ctx context.Context, db *sql.DB, hash string) (numVotes int16, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockVoteCount, hash).Scan(&numVotes) return } // RetrieveBlocksHashesAll retrieve the hash of every block in the blocks table, // ordered by their row ID. -func RetrieveBlocksHashesAll(db *sql.DB) ([]string, error) { +func RetrieveBlocksHashesAll(ctx context.Context, db *sql.DB) ([]string, error) { var hashes []string - rows, err := db.Query(internal.SelectBlocksHashes) + rows, err := db.QueryContext(ctx, internal.SelectBlocksHashes) if err != nil { return hashes, err } @@ -2576,16 +2607,16 @@ func RetrieveBlocksHashesAll(db *sql.DB) ([]string, error) { // RetrieveBlockChainDbID retrieves the row id in the block_chain table of the // block with the given hash, if it exists (be sure to check error against // sql.ErrNoRows!). -func RetrieveBlockChainDbID(db *sql.DB, hash string) (dbID uint64, err error) { - err = db.QueryRow(internal.SelectBlockChainRowIDByHash, hash).Scan(&dbID) +func RetrieveBlockChainDbID(ctx context.Context, db *sql.DB, hash string) (dbID uint64, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockChainRowIDByHash, hash).Scan(&dbID) return } // RetrieveSideChainBlocks retrieves the block chain status for all known side // chain blocks. -func RetrieveSideChainBlocks(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { +func RetrieveSideChainBlocks(ctx context.Context, db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectSideChainBlocks) + rows, err = db.QueryContext(ctx, internal.SelectSideChainBlocks) if err != nil { return } @@ -2605,9 +2636,9 @@ func RetrieveSideChainBlocks(db *sql.DB) (blocks []*dbtypes.BlockStatus, err err // RetrieveSideChainTips retrieves the block chain status for all known side // chain tip blocks. -func RetrieveSideChainTips(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { +func RetrieveSideChainTips(ctx context.Context, db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectSideChainTips) + rows, err = db.QueryContext(ctx, internal.SelectSideChainTips) if err != nil { return } @@ -2628,9 +2659,9 @@ func RetrieveSideChainTips(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error // RetrieveDisapprovedBlocks retrieves the block chain status for all blocks // that had their regular transactions invalidated by stakeholder disapproval. -func RetrieveDisapprovedBlocks(db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { +func RetrieveDisapprovedBlocks(ctx context.Context, db *sql.DB) (blocks []*dbtypes.BlockStatus, err error) { var rows *sql.Rows - rows, err = db.Query(internal.SelectDisapprovedBlocks) + rows, err = db.QueryContext(ctx, internal.SelectDisapprovedBlocks) if err != nil { return } @@ -2650,19 +2681,23 @@ func RetrieveDisapprovedBlocks(db *sql.DB) (blocks []*dbtypes.BlockStatus, err e // RetrieveBlockStatus retrieves the block chain status for the block with the // specified hash. -func RetrieveBlockStatus(db *sql.DB, hash string) (bs dbtypes.BlockStatus, err error) { - err = db.QueryRow(internal.SelectBlockStatus, hash).Scan(&bs.IsValid, +func RetrieveBlockStatus(ctx context.Context, db *sql.DB, hash string) (bs dbtypes.BlockStatus, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockStatus, hash).Scan(&bs.IsValid, &bs.IsMainchain, &bs.Height, &bs.PrevHash, &bs.Hash, &bs.NextHash) return } // RetrieveBlockFlags retrieves the block's is_valid and is_mainchain flags. -func RetrieveBlockFlags(db *sql.DB, hash string) (isValid bool, isMainchain bool, err error) { - err = db.QueryRow(internal.SelectBlockFlags, hash).Scan(&isValid, &isMainchain) +func RetrieveBlockFlags(ctx context.Context, db *sql.DB, hash string) (isValid bool, isMainchain bool, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlockFlags, hash).Scan(&isValid, &isMainchain) return } -func RetrieveBlockSummaryByTimeRange(db *sql.DB, minTime, maxTime int64, limit int) ([]dbtypes.BlockDataBasic, error) { +// RetrieveBlockSummaryByTimeRange retrieves the slice of block summaries for +// the given time range. The limit specifies the number of most recent block +// summaries to return. A limit of 0 indicates all blocks in the time range +// should be included. +func RetrieveBlockSummaryByTimeRange(ctx context.Context, db *sql.DB, minTime, maxTime int64, limit int) ([]dbtypes.BlockDataBasic, error) { var blocks []dbtypes.BlockDataBasic var stmt *sql.Stmt var rows *sql.Rows @@ -2673,13 +2708,13 @@ func RetrieveBlockSummaryByTimeRange(db *sql.DB, minTime, maxTime int64, limit i if err != nil { return nil, err } - rows, err = stmt.Query(minTime, maxTime) + rows, err = stmt.QueryContext(ctx, minTime, maxTime) } else { stmt, err = db.Prepare(internal.SelectBlockByTimeRangeSQL) if err != nil { return nil, err } - rows, err = stmt.Query(minTime, maxTime, limit) + rows, err = stmt.QueryContext(ctx, minTime, maxTime, limit) } if err != nil { @@ -2706,8 +2741,8 @@ func RetrieveBlockSummaryByTimeRange(db *sql.DB, minTime, maxTime int64, limit i // RetrieveTicketsPriceByHeight fetches the ticket price and its timestamp that // are used to display the ticket price variation on ticket price chart. These // data are fetched at an interval of chaincfg.Params.StakeDiffWindowSize. -func RetrieveTicketsPriceByHeight(db *sql.DB, val int64) (*dbtypes.ChartsData, error) { - rows, err := db.Query(internal.SelectBlocksTicketsPrice, val) +func RetrieveTicketsPriceByHeight(ctx context.Context, db *sql.DB, val int64) (*dbtypes.ChartsData, error) { + rows, err := db.QueryContext(ctx, internal.SelectBlocksTicketsPrice, val) if err != nil { return nil, err } @@ -2732,18 +2767,22 @@ func RetrieveTicketsPriceByHeight(db *sql.DB, val int64) (*dbtypes.ChartsData, e return items, nil } -func RetrievePreviousHashByBlockHash(db *sql.DB, hash string) (previousHash string, err error) { - err = db.QueryRow(internal.SelectBlocksPreviousHash, hash).Scan(&previousHash) +// RetrievePreviousHashByBlockHash retrieves the previous block hash for the +// given block from the blocks table. +func RetrievePreviousHashByBlockHash(ctx context.Context, db *sql.DB, hash string) (previousHash string, err error) { + err = db.QueryRowContext(ctx, internal.SelectBlocksPreviousHash, hash).Scan(&previousHash) return } +// SetMainchainByBlockHash is used to set the is_mainchain flag for the given +// block. This is required to handle a reoganization. func SetMainchainByBlockHash(db *sql.DB, hash string, isMainchain bool) (previousHash string, err error) { err = db.QueryRow(internal.UpdateBlockMainchain, hash, isMainchain).Scan(&previousHash) return } -func retrieveBlockTicketsPoolValue(db *sql.DB) (*dbtypes.ChartsData, error) { - rows, err := db.Query(internal.SelectBlocksBlockSize) +func retrieveBlockTicketsPoolValue(ctx context.Context, db *sql.DB) (*dbtypes.ChartsData, error) { + rows, err := db.QueryContext(ctx, internal.SelectBlocksBlockSize) if err != nil { return nil, err } @@ -2902,7 +2941,10 @@ func UpdateLastBlockValid(db *sql.DB, blockDbID uint64, isValid bool) error { // table for all of the transactions in the block specified by the given block // hash. func UpdateLastVins(db *sql.DB, blockHash string, isValid, isMainchain bool) error { - _, txs, _, trees, timestamps, err := RetrieveTxsByBlockHash(db, blockHash) + // Retrieve the hash for every transaction in this block. A context with no + // deadline or cancellation function is used since this UpdateLastVins needs + // to complete to ensure DB integrity. + _, txs, _, trees, timestamps, err := RetrieveTxsByBlockHash(context.Background(), db, blockHash) if err != nil { return err } @@ -2927,9 +2969,13 @@ func UpdateLastVins(db *sql.DB, blockHash string, isValid, isMainchain bool) err // addresses table rows pertaining to regular (non-stake) transactions found in // the given block. func UpdateLastAddressesValid(db *sql.DB, blockHash string, isValid bool) error { + // The queries in this function should not timeout or (probably) canceled, + // so use a background context. + ctx := context.Background() + // Get the row ids of all vins and vouts of regular txns in this block. onlyRegularTxns := true - vinDbIDsBlk, voutDbIDsBlk, _, err := RetrieveTxnsVinsVoutsByBlock(db, blockHash, onlyRegularTxns) + vinDbIDsBlk, voutDbIDsBlk, _, err := RetrieveTxnsVinsVoutsByBlock(ctx, db, blockHash, onlyRegularTxns) if err != nil { return fmt.Errorf("unable to retrieve vin data for block %s: %v", blockHash, err) } diff --git a/db/dcrpg/sync.go b/db/dcrpg/sync.go index 3217922e2..9eb73e638 100644 --- a/db/dcrpg/sync.go +++ b/db/dcrpg/sync.go @@ -154,6 +154,9 @@ func (db *ChainDB) SyncChainDB(ctx context.Context, client rpcutils.MasterBlockG } totalVoutPerSec := totalVouts / int64(totalElapsed) totalTxPerSec := totalTxs / int64(totalElapsed) + if totalTxs == 0 { + return + } log.Infof("Avg. speed: %d tx/s, %d vout/s", totalTxPerSec, totalVoutPerSec) } speedReport := func() { o.Do(speedReporter) } @@ -320,9 +323,10 @@ func (db *ChainDB) SyncChainDB(ctx context.Context, client rpcutils.MasterBlockG // and clear the general address balance cache. if err = db.FreshenAddressCaches(false); err != nil { log.Warnf("FreshenAddressCaches: %v", err) + err = nil // not an error with sync } - // Signal the end of the initial load sync + // Signal the end of the initial load sync. if barLoad != nil { barLoad <- &dbtypes.ProgressBarLoad{ From: nodeHeight, diff --git a/db/dcrpg/upgrades.go b/db/dcrpg/upgrades.go index 71370402e..2af45e7fa 100644 --- a/db/dcrpg/upgrades.go +++ b/db/dcrpg/upgrades.go @@ -5,6 +5,7 @@ package dcrpg import ( "bytes" + "context" "database/sql" "fmt" "time" @@ -470,7 +471,7 @@ func (pgb *ChainDB) handleUpgrades(client *rpcutils.BlockGate, case blocksTableMainchainUpgrade: var blockHash string // blocks table upgrade proceeds from best block back to genesis - _, blockHash, _, err = RetrieveBestBlockHeightAny(pgb.db) + _, blockHash, _, err = RetrieveBestBlockHeightAny(context.Background(), pgb.db) if err != nil { return false, fmt.Errorf("failed to retrieve best block from DB: %v", err) } @@ -655,9 +656,13 @@ func (pgb *ChainDB) handleAgendasVotingMilestonesUpgrade() (int64, error) { } func (pgb *ChainDB) handleVinsTableMainchainupgrade() (int64, error) { + // The queries in this function should not timeout or (probably) canceled, + // so use a background context. + ctx := context.Background() + // Get all of the block hashes log.Infof(" - Retrieving all block hashes...") - blockHashes, err := RetrieveBlocksHashesAll(pgb.db) + blockHashes, err := RetrieveBlocksHashesAll(ctx, pgb.db) if err != nil { return 0, fmt.Errorf("unable to retrieve all block hashes: %v", err) } @@ -665,7 +670,7 @@ func (pgb *ChainDB) handleVinsTableMainchainupgrade() (int64, error) { log.Infof(" - Updating vins data for each transactions in every block...") var rowsUpdated int64 for i, blockHash := range blockHashes { - vinDbIDsBlk, areValid, areMainchain, err := RetrieveTxnsVinsByBlock(pgb.db, blockHash) + vinDbIDsBlk, areValid, areMainchain, err := RetrieveTxnsVinsByBlock(ctx, pgb.db, blockHash) if err != nil { return 0, fmt.Errorf("unable to retrieve vin data for block %s: %v", blockHash, err) } diff --git a/explorer/explorer.go b/explorer/explorer.go index 6b24e73bc..773b926be 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -78,12 +78,12 @@ type explorerDataSource interface { BlockMissedVotes(blockHash string) ([]string, error) AgendaVotes(agendaID string, chartType int) (*dbtypes.AgendaVoteChoices, error) GetPgChartsData() (map[string]*dbtypes.ChartsData, error) - GetTicketsPriceByHeight() (*dbtypes.ChartsData, error) + TicketsPriceByHeight() (*dbtypes.ChartsData, error) SideChainBlocks() ([]*dbtypes.BlockStatus, error) DisapprovedBlocks() ([]*dbtypes.BlockStatus, error) BlockStatus(hash string) (dbtypes.BlockStatus, error) BlockFlags(hash string) (bool, bool, error) - GetAddressMetrics(addr string) (*dbtypes.AddressMetrics, error) + AddressMetrics(addr string) (*dbtypes.AddressMetrics, error) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ([]*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) TransactionBlocks(hash string) ([]*dbtypes.BlockStatus, []uint32, error) Transaction(txHash string) ([]*dbtypes.Tx, error) @@ -429,6 +429,10 @@ func (exp *explorerUI) prePopulateChartsData() { log.Info("Pre-populating the charts data. This may take a minute...") var err error pgData, err := exp.explorerSource.GetPgChartsData() + if dbtypes.IsTimeoutErr(err) { + log.Warnf("GetPgChartsData DB timeout: %v", err) + return + } if err != nil { log.Errorf("Invalid PG data found: %v", err) return diff --git a/explorer/explorermiddleware.go b/explorer/explorermiddleware.go index ad0fb6e73..f5fa502ac 100644 --- a/explorer/explorermiddleware.go +++ b/explorer/explorermiddleware.go @@ -37,9 +37,12 @@ func (exp *explorerUI) BlockHashPathOrIndexCtx(next http.Handler) http.Handler { } else { height, err = exp.explorerSource.BlockHeight(hash) } + if exp.timeoutErrorPage(w, err, "BlockHashPathOrIndexCtx>BlockHeight") { + return + } if err != nil { log.Errorf("BlockHeight(%s) failed: %v", hash, err) - exp.StatusPage(w, defaultErrorCode, "could not find that block", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find that block", ExpStatusNotFound) return } } else { @@ -52,7 +55,7 @@ func (exp *explorerUI) BlockHashPathOrIndexCtx(next http.Handler) http.Handler { if err != nil { log.Errorf("HeightDB() failed: %v", err) exp.StatusPage(w, defaultErrorCode, - "an unexpected error had occured while retrieving the best block", ErrorStatusType) + "an unexpected error had occured while retrieving the best block", ExpStatusError) return } maxHeight = int64(bestBlockHeight) @@ -61,7 +64,7 @@ func (exp *explorerUI) BlockHashPathOrIndexCtx(next http.Handler) http.Handler { if height > maxHeight { expectedTime := time.Duration(height-maxHeight) * exp.ChainParams.TargetTimePerBlock message := fmt.Sprintf("This block is expected to arrive in approximately in %v. ", expectedTime) - exp.StatusPage(w, defaultErrorCode, message, FutureBlockStatusType) + exp.StatusPage(w, defaultErrorCode, message, ExpStatusFutureBlock) return } @@ -74,7 +77,7 @@ func (exp *explorerUI) BlockHashPathOrIndexCtx(next http.Handler) http.Handler { } if err != nil { log.Errorf("%s(%d) failed: %v", f, height, err) - exp.StatusPage(w, defaultErrorCode, "could not find that block", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find that block", ExpStatusNotFound) return } } @@ -93,7 +96,7 @@ func (exp *explorerUI) SyncStatusPageActivation(next http.Handler) http.Handler return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if exp.DisplaySyncStatusPage() { exp.StatusPage(w, "Database Update Running. Please Wait...", - "Blockchain sync is running. Please wait ...", BlockchainSyncingType) + "Blockchain sync is running. Please wait ...", ExpStatusSyncing) } else { // Pass the token to the next middleware handler next.ServeHTTP(w, r) diff --git a/explorer/explorerroutes.go b/explorer/explorerroutes.go index f3a095c6c..8a6164aca 100644 --- a/explorer/explorerroutes.go +++ b/explorer/explorerroutes.go @@ -50,6 +50,16 @@ func netName(chainParams *chaincfg.Params) string { return strings.Title(chainParams.Name) } +func (exp *explorerUI) timeoutErrorPage(w http.ResponseWriter, err error, debugStr string) (wasTimeout bool) { + wasTimeout = dbtypes.IsTimeoutErr(err) + if wasTimeout { + log.Debugf("%s: %v", debugStr, err) + exp.StatusPage(w, defaultErrorCode, + "Database timeout. Please try again later.", ExpStatusDBTimeout) + } + return +} + // Home is the page handler for the "/" path. func (exp *explorerUI) Home(w http.ResponseWriter, r *http.Request) { height := exp.blockData.GetHeight() @@ -79,7 +89,7 @@ func (exp *explorerUI) Home(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -90,9 +100,12 @@ func (exp *explorerUI) Home(w http.ResponseWriter, r *http.Request) { // SideChains is the page handler for the "/side" path. func (exp *explorerUI) SideChains(w http.ResponseWriter, r *http.Request) { sideBlocks, err := exp.explorerSource.SideChainBlocks() + if exp.timeoutErrorPage(w, err, "SideChainBlocks") { + return + } if err != nil { log.Errorf("Unable to get side chain blocks: %v", err) - exp.StatusPage(w, defaultErrorCode, "failed to retrieve side chain blocks", ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, "failed to retrieve side chain blocks", ExpStatusError) return } @@ -108,7 +121,7 @@ func (exp *explorerUI) SideChains(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -119,10 +132,13 @@ func (exp *explorerUI) SideChains(w http.ResponseWriter, r *http.Request) { // DisapprovedBlocks is the page handler for the "/rejects" path. func (exp *explorerUI) DisapprovedBlocks(w http.ResponseWriter, r *http.Request) { disapprovedBlocks, err := exp.explorerSource.DisapprovedBlocks() + if exp.timeoutErrorPage(w, err, "DisapprovedBlocks") { + return + } if err != nil { log.Errorf("Unable to get stakeholder disapproved blocks: %v", err) exp.StatusPage(w, defaultErrorCode, - "failed to retrieve stakeholder disapproved blocks", ErrorStatusType) + "failed to retrieve stakeholder disapproved blocks", ExpStatusError) return } @@ -138,7 +154,7 @@ func (exp *explorerUI) DisapprovedBlocks(w http.ResponseWriter, r *http.Request) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -276,7 +292,7 @@ func (exp *explorerUI) NextHome(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -284,11 +300,11 @@ func (exp *explorerUI) NextHome(w http.ResponseWriter, r *http.Request) { io.WriteString(w, str) } -// StakeDiffWindows is the page handler for the "/ticketpricewindows" path +// StakeDiffWindows is the page handler for the "/ticketpricewindows" path. func (exp *explorerUI) StakeDiffWindows(w http.ResponseWriter, r *http.Request) { if exp.liteMode { exp.StatusPage(w, fullModeRequired, - "Windows page cannot run in lite mode.", NotSupportedStatusType) + "Windows page cannot run in lite mode.", ExpStatusNotSupported) } offsetWindow, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64) @@ -311,9 +327,12 @@ func (exp *explorerUI) StakeDiffWindows(w http.ResponseWriter, r *http.Request) } windows, err := exp.explorerSource.PosIntervals(rows, offsetWindow) + if exp.timeoutErrorPage(w, err, "PosIntervals") { + return + } if err != nil { log.Errorf("The specified windows are invalid. offset=%d&rows=%d: error: %v ", offsetWindow, rows, err) - exp.StatusPage(w, defaultErrorCode, "The specified windows could not found", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "The specified windows could not found", ExpStatusNotFound) return } @@ -337,7 +356,7 @@ func (exp *explorerUI) StakeDiffWindows(w http.ResponseWriter, r *http.Request) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -366,11 +385,12 @@ func (exp *explorerUI) YearBlocksListing(w http.ResponseWriter, r *http.Request) exp.timeBasedBlocksListing("Years", w, r) } -// TimeBasedBlocksListing is the main handler for "/day", "/week", "/month" and "/year". +// TimeBasedBlocksListing is the main handler for "/day", "/week", "/month" and +// "/year". func (exp *explorerUI) timeBasedBlocksListing(val string, w http.ResponseWriter, r *http.Request) { if exp.liteMode { exp.StatusPage(w, fullModeRequired, - "Time based blocks listing page cannot run in lite mode.", NotSupportedStatusType) + "Time based blocks listing page cannot run in lite mode.", ExpStatusNotSupported) } grouping := dbtypes.TimeGroupingFromStr(val) @@ -379,7 +399,7 @@ func (exp *explorerUI) timeBasedBlocksListing(val string, w http.ResponseWriter, // default to year grouping if grouping is missing i, err = dbtypes.TimeBasedGroupingToInterval(dbtypes.YearGrouping) if err != nil { - exp.StatusPage(w, defaultErrorCode, "Invalid year grouping found.", ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, "Invalid year grouping found.", ExpStatusError) log.Errorf("Invalid year grouping found: error: %v ", err) } grouping = dbtypes.YearGrouping @@ -407,9 +427,12 @@ func (exp *explorerUI) timeBasedBlocksListing(val string, w http.ResponseWriter, } data, err := exp.explorerSource.TimeBasedIntervals(grouping, rows, offset) + if exp.timeoutErrorPage(w, err, "TimeBasedIntervals") { + return + } if err != nil { log.Errorf("The specified /%s intervals are invalid. offset=%d&rows=%d: error: %v ", val, offset, rows, err) - exp.StatusPage(w, defaultErrorCode, "The specified intervals could not found", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "The specified intervals could not found", ExpStatusNotFound) return } @@ -433,7 +456,7 @@ func (exp *explorerUI) timeBasedBlocksListing(val string, w http.ResponseWriter, if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -468,13 +491,16 @@ func (exp *explorerUI) Blocks(w http.ResponseWriter, r *http.Request) { summaries := exp.blockData.GetExplorerBlocks(height, height-rows) if summaries == nil { log.Errorf("Unable to get blocks: height=%d&rows=%d", height, rows) - exp.StatusPage(w, defaultErrorCode, "could not find those blocks", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find those blocks", ExpStatusNotFound) return } if !exp.liteMode { for _, s := range summaries { blockStatus, err := exp.explorerSource.BlockStatus(s.Hash) + if exp.timeoutErrorPage(w, err, "BlockStatus") { + return + } if err != nil && err != sql.ErrNoRows { log.Warnf("Unable to retrieve chain status for block %s: %v", s.Hash, err) } @@ -501,7 +527,7 @@ func (exp *explorerUI) Blocks(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -516,7 +542,7 @@ func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) { data := exp.blockData.GetExplorerBlock(hash) if data == nil { log.Errorf("Unable to get block %s", hash) - exp.StatusPage(w, defaultErrorCode, "could not find that block", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find that block", ExpStatusNotFound) return } @@ -537,12 +563,18 @@ func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) { if !exp.liteMode { var err error data.Misses, err = exp.explorerSource.BlockMissedVotes(hash) + if exp.timeoutErrorPage(w, err, "BlockMissedVotes") { + return + } if err != nil && err != sql.ErrNoRows { log.Warnf("Unable to retrieve missed votes for block %s: %v", hash, err) } var blockStatus dbtypes.BlockStatus blockStatus, err = exp.explorerSource.BlockStatus(hash) + if exp.timeoutErrorPage(w, err, "BlockStatus") { + return + } if err != nil && err != sql.ErrNoRows { log.Warnf("Unable to retrieve chain status for block %s: %v", hash, err) } @@ -562,7 +594,7 @@ func (exp *explorerUI) Block(w http.ResponseWriter, r *http.Request) { str, err := exp.templates.execTemplateToString("block", pageData) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -587,7 +619,7 @@ func (exp *explorerUI) Mempool(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -599,15 +631,18 @@ func (exp *explorerUI) Mempool(w http.ResponseWriter, r *http.Request) { func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) { if exp.liteMode { exp.StatusPage(w, fullModeRequired, - "Ticketpool page cannot run in lite mode", NotSupportedStatusType) + "Ticketpool page cannot run in lite mode", ExpStatusNotSupported) return } interval := dbtypes.AllGrouping barGraphs, donutChart, height, err := exp.explorerSource.TicketPoolVisualization(interval) + if exp.timeoutErrorPage(w, err, "TicketPoolVisualization") { + return + } if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -641,7 +676,7 @@ func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -656,13 +691,13 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { hash, ok := r.Context().Value(ctxTxHash).(string) if !ok { log.Trace("txid not set") - exp.StatusPage(w, defaultErrorCode, "there was no transaction requested", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "there was no transaction requested", ExpStatusNotFound) return } inout, _ := r.Context().Value(ctxTxInOut).(string) if inout != "in" && inout != "out" && inout != "" { - exp.StatusPage(w, defaultErrorCode, "there was no transaction requested", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "there was no transaction requested", ExpStatusNotFound) return } ioid, _ := r.Context().Value(ctxTxInOutId).(string) @@ -674,18 +709,21 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { if tx == nil { if exp.liteMode { log.Errorf("Unable to get transaction %s", hash) - exp.StatusPage(w, defaultErrorCode, "could not find that transaction", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find that transaction", ExpStatusNotFound) return } // Search for occurrences of the transaction in the database. dbTxs, err := exp.explorerSource.Transaction(hash) + if exp.timeoutErrorPage(w, err, "Transaction") { + return + } if err != nil { log.Errorf("Unable to retrieve transaction details for %s.", hash) - exp.StatusPage(w, defaultErrorCode, "could not find that transaction", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "could not find that transaction", ExpStatusNotFound) return } if dbTxs == nil { - exp.StatusPage(w, defaultErrorCode, "that transaction has not been recorded", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "that transaction has not been recorded", ExpStatusNotFound) return } @@ -722,10 +760,13 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { // Retrieve vouts from DB. vouts, err := exp.explorerSource.VoutsForTx(dbTx0) + if exp.timeoutErrorPage(w, err, "VoutsForTx") { + return + } if err != nil { log.Errorf("Failed to retrieve all vout details for transaction %s: %v", dbTx0.TxID, err) - exp.StatusPage(w, defaultErrorCode, "VoutsForTx failed", ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, "VoutsForTx failed", ExpStatusError) return } @@ -739,6 +780,9 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { } // Determine if the outpoint is spent spendingTx, _, _, err := exp.explorerSource.SpendingTransaction(hash, vouts[iv].TxIndex) + if exp.timeoutErrorPage(w, err, "SpendingTransaction") { + return + } if err != nil && err != sql.ErrNoRows { log.Warnf("SpendingTransaction failed for outpoint %s:%d: %v", hash, vouts[iv].TxIndex, err) @@ -757,10 +801,13 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { // Retrieve vins from DB. vins, prevPkScripts, scriptVersions, err := exp.explorerSource.VinsForTx(dbTx0) + if exp.timeoutErrorPage(w, err, "VinsForTx") { + return + } if err != nil { log.Errorf("Failed to retrieve all vin details for transaction %s: %v", dbTx0.TxID, err) - exp.StatusPage(w, defaultErrorCode, "VinsForTx failed", ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, "VinsForTx failed", ExpStatusError) return } @@ -887,9 +934,12 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { // Details on all the blocks containing this transaction var err error blocks, blockInds, err = exp.explorerSource.TransactionBlocks(tx.TxID) + if exp.timeoutErrorPage(w, err, "TransactionBlocks") { + return + } if err != nil { log.Errorf("Unable to retrieve blocks for transaction %s: %v", hash, err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -905,9 +955,12 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { // For each output of this transaction, look up any spending transactions, // and the index of the spending transaction input. spendingTxHashes, spendingTxVinInds, voutInds, err := exp.explorerSource.SpendingTransactions(hash) + if exp.timeoutErrorPage(w, err, "SpendingTransactions") { + return + } if err != nil { log.Errorf("Unable to retrieve spending transactions for %s: %v", hash, err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } for i, vout := range voutInds { @@ -922,8 +975,15 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { } if tx.IsTicket() { spendStatus, poolStatus, err := exp.explorerSource.PoolStatusForTicket(hash) - if err != nil { + if exp.timeoutErrorPage(w, err, "PoolStatusForTicket") { + return + } + if err != nil && err != sql.ErrNoRows { log.Errorf("Unable to retrieve ticket spend and pool status for %s: %v", hash, err) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) + return + } else if err != sql.ErrNoRows { + log.Warnf("Spend and pool status not found for ticket %s: %v", hash, err) } else { if tx.Mature == "False" { tx.TicketInfo.PoolStatus = "immature" @@ -1026,7 +1086,7 @@ func (exp *explorerUI) TxPage(w http.ResponseWriter, r *http.Request) { str, err := exp.templates.execTemplateToString("tx", pageData) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -1053,7 +1113,7 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { address, ok := r.Context().Value(ctxAddress).(string) if !ok { log.Trace("address not set") - exp.StatusPage(w, defaultErrorCode, "there seems to not be an address in this request", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, "there seems to not be an address in this request", ExpStatusNotFound) return } @@ -1082,7 +1142,7 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { } txnType := dbtypes.AddrTxnTypeFromStr(txntype) if txnType == dbtypes.AddrTxnUnknown { - exp.StatusPage(w, defaultErrorCode, "unknown txntype query value", ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, "unknown txntype query value", ExpStatusError) return } log.Debugf("Showing transaction types: %s (%d)", txntype, txnType) @@ -1094,31 +1154,47 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { if exp.liteMode { addrData, err = exp.blockData.GetExplorerAddress(address, limitN, offsetAddrOuts) if err != nil && strings.HasPrefix(err.Error(), "wrong network") { - exp.StatusPage(w, wrongNetwork, "That address is not valid for "+exp.NetName, NotSupportedStatusType) + exp.StatusPage(w, wrongNetwork, "That address is not valid for "+exp.NetName, ExpStatusNotSupported) return } - if err != nil { - log.Errorf("Unable to get address %s: %v", address, err) - exp.StatusPage(w, defaultErrorCode, "Unexpected issue locating data for that address.", ErrorStatusType) - return - } - if addrData == nil { - exp.StatusPage(w, defaultErrorCode, "Unknown issue locating data for that address.", NotFoundStatusType) + // AddressInfo should never be nil if err is nil. Catch non-nil error + // and nil addrData here since they are both unexpected errors. + if err != nil || addrData == nil { + log.Errorf("Unable to get data for address %s: %v", address, err) + exp.StatusPage(w, defaultErrorCode, + "Unexpected issue locating data for that address.", ExpStatusError) return } } else { - // Get addresses table rows for the address + // Get addresses table rows for the address. addrHist, balance, errH := exp.explorerSource.AddressHistory( address, limitN, offsetAddrOuts, txnType) - - if errH == nil { - // Generate AddressInfo skeleton from the address table rows + if exp.timeoutErrorPage(w, errH, "AddressHistory") { + return + } else if errH == sql.ErrNoRows { + // We do not have any confirmed transactions. Prep to display ONLY + // unconfirmed transactions (or none at all). + addrData = new(AddressInfo) + addrData.Address = address + addrData.Fullmode = true + addrData.Balance = &AddressBalance{} + log.Tracef("AddressHistory: No confirmed transactions for address %s.", address) + } else if errH != nil { + // Unexpected error + log.Errorf("AddressHistory: %v", errH) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) + return + } else /*errH == nil*/ { + // Generate AddressInfo skeleton from the address table rows. addrData = ReduceAddressHistory(addrHist) if addrData == nil { // Empty history is not expected for credit txnType with any txns. - if txnType != dbtypes.AddrTxnDebit && (balance.NumSpent+balance.NumUnspent) > 0 { - log.Debugf("empty address history (%s): n=%d&start=%d", address, limitN, offsetAddrOuts) - exp.StatusPage(w, defaultErrorCode, "that address has no history", NotFoundStatusType) + if txnType != dbtypes.AddrTxnDebit && + (balance.NumSpent+balance.NumUnspent) > 0 { + log.Debugf("empty address history (%s): n=%d&start=%d", + address, limitN, offsetAddrOuts) + exp.StatusPage(w, defaultErrorCode, + "that address has no history", ExpStatusNotFound) return } // No mined transactions @@ -1152,53 +1228,59 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { addrData.NumTransactions = limitN } - // Query database for transaction details + // Query database for transaction details. err = exp.explorerSource.FillAddressTransactions(addrData) + if exp.timeoutErrorPage(w, err, "FillAddressTransactions") { + return + } if err != nil { log.Errorf("Unable to fill address %s transactions: %v", address, err) - exp.StatusPage(w, defaultErrorCode, "could not find transactions for that address", NotFoundStatusType) + exp.StatusPage(w, defaultErrorCode, + "transactions for that address not found", ExpStatusNotFound) return } - } else { - // We do not have any confirmed transactions. Prep to display ONLY - // unconfirmed transactions (or none at all) - addrData = new(AddressInfo) - addrData.Address = address - addrData.Fullmode = true - addrData.Balance = &AddressBalance{} } - // If there are confirmed transactions, check the oldest transaction's time. + // If there are confirmed transactions, get address metrics: oldest + // transaction's time, number of entries for the various time + // groupings/intervals. if len(addrData.Transactions) > 0 { - addrMetrics, err = exp.explorerSource.GetAddressMetrics(address) + addrMetrics, err = exp.explorerSource.AddressMetrics(address) + if exp.timeoutErrorPage(w, err, "AddressMetrics") { + return + } if err != nil { log.Errorf("Unable to fetch address metrics %s: %v", address, err) exp.StatusPage(w, defaultErrorCode, "address metrics not found", - NotFoundStatusType) + ExpStatusNotFound) return } } else { addrMetrics = &dbtypes.AddressMetrics{} } - // Check for unconfirmed transactions + // Check for unconfirmed transactions. addressOuts, numUnconfirmed, err := exp.blockData.UnconfirmedTxnsForAddress(address) - if err != nil { - log.Warnf("UnconfirmedTxnsForAddress failed for address %s: %v", address, err) + if err != nil || addressOuts == nil { + log.Errorf("UnconfirmedTxnsForAddress failed for address %s: %v", address, err) + exp.StatusPage(w, defaultErrorCode, "transactions for that address not found", + ExpStatusNotFound) + return } addrData.NumUnconfirmed = numUnconfirmed if addrData.UnconfirmedTxns == nil { addrData.UnconfirmedTxns = new(AddressTransactions) } + // Funding transactions (unconfirmed) var received, sent, numReceived, numSent int64 FUNDING_TX_DUPLICATE_CHECK: for _, f := range addressOuts.Outpoints { - //Mempool transactions stick around for 2 blocks. The first block - //incorporates the transaction and mines it. The second block - //validates it by the stake. However, transactions move into our - //database as soon as they are mined and thus we need to be careful - //to not include those transactions in our list. + // Mempool transactions stick around for 2 blocks. The first block + // incorporates the transaction and mines it. The second block + // validates it by the stake. However, transactions move into our + // database as soon as they are mined and thus we need to be careful + // to not include those transactions in our list. for _, b := range addrData.Transactions { if f.Hash.String() == b.TxID && f.Index == b.InOutID { continue FUNDING_TX_DUPLICATE_CHECK @@ -1229,14 +1311,15 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { numReceived++ } + // Spending transactions (unconfirmed) SPENDING_TX_DUPLICATE_CHECK: for _, f := range addressOuts.PrevOuts { - //Mempool transactions stick around for 2 blocks. The first block - //incorporates the transaction and mines it. The second block - //validates it by the stake. However, transactions move into our - //database as soon as they are mined and thus we need to be careful - //to not include those transactions in our list. + // Mempool transactions stick around for 2 blocks. The first block + // incorporates the transaction and mines it. The second block + // validates it by the stake. However, transactions move into our + // database as soon as they are mined and thus we need to be careful + // to not include those transactions in our list. for _, b := range addrData.Transactions { if f.TxSpending.String() == b.TxID && f.InputIndex == int(b.InOutID) { continue SPENDING_TX_DUPLICATE_CHECK @@ -1252,15 +1335,15 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { continue } - // sent total sats has to be a lookup of the vout:i prevout value - // because vin:i valuein is not reliable from dcrd at present + // The total send amount must be looked up from the previous + // outpoint because vin:i valuein is not reliable from dcrd. prevhash := spendingTx.Tx.TxIn[f.InputIndex].PreviousOutPoint.Hash strprevhash := prevhash.String() previndex := spendingTx.Tx.TxIn[f.InputIndex].PreviousOutPoint.Index valuein := addressOuts.TxnsStore[prevhash].Tx.TxOut[previndex].Value - // Look through old transactions and set the - // the spending transactions match fields + // Look through old transactions and set the spending transactions' + // matching transaction fields. for _, dbTxn := range addrData.Transactions { if dbTxn.TxID == strprevhash && dbTxn.InOutID == previndex && dbTxn.IsFunding { dbTxn.MatchedTx = spendingTx.Hash().String() @@ -1285,22 +1368,16 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { sent += valuein numSent++ - } + } // range addressOuts.PrevOuts + + // Totals from funding and spending transactions. addrData.Balance.NumSpent += numSent addrData.Balance.NumUnspent += (numReceived - numSent) addrData.Balance.TotalSpent += sent addrData.Balance.TotalUnspent += (received - sent) - - if err != nil { - log.Errorf("Unable to fetch transactions for the address %s: %v", address, err) - exp.StatusPage(w, defaultErrorCode, "transactions for that address not found", - NotFoundStatusType) - return - } - } - // Set page parameters + // Set page parameters. addrData.Path = r.URL.Path addrData.Limit, addrData.Offset = limitN, offsetAddrOuts addrData.TxnType = txnType.String() @@ -1312,13 +1389,15 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { return addrData.Transactions[i].Time.T.Unix() > addrData.Transactions[j].Time.T.Unix() }) - // Do not put this before the sort.Slice of addrData.Transactions above + // Compute block height for each transaction. This must be done *after* + // sort.Slice of addrData.Transactions. txBlockHeights := make([]int64, len(addrData.Transactions)) bdHeight := exp.Height() for i, v := range addrData.Transactions { txBlockHeights[i] = bdHeight - int64(v.Confirmations) + 1 } + // Execute the HTML template. pageData := AddressPageData{ CommonPageData: exp.commonData(), Data: addrData, @@ -1330,9 +1409,10 @@ func (exp *explorerUI) AddressPage(w http.ResponseWriter, r *http.Request) { str, err := exp.templates.execTemplateToString("address", pageData) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } + w.Header().Set("Content-Type", "text/html") w.Header().Set("Turbolinks-Location", r.URL.RequestURI()) w.WriteHeader(http.StatusOK) @@ -1351,7 +1431,7 @@ func (exp *explorerUI) DecodeTxPage(w http.ResponseWriter, r *http.Request) { }) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -1363,13 +1443,16 @@ func (exp *explorerUI) DecodeTxPage(w http.ResponseWriter, r *http.Request) { func (exp *explorerUI) Charts(w http.ResponseWriter, r *http.Request) { if exp.liteMode { exp.StatusPage(w, fullModeRequired, - "Charts page cannot run in lite mode", NotSupportedStatusType) + "Charts page cannot run in lite mode", ExpStatusNotSupported) + return + } + tickets, err := exp.explorerSource.TicketsPriceByHeight() + if exp.timeoutErrorPage(w, err, "TicketsPriceByHeight") { return } - tickets, err := exp.explorerSource.GetTicketsPriceByHeight() if err != nil { log.Errorf("Loading the Ticket Price By Height chart data failed %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -1384,7 +1467,7 @@ func (exp *explorerUI) Charts(w http.ResponseWriter, r *http.Request) { }) if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -1399,7 +1482,7 @@ func (exp *explorerUI) Charts(w http.ResponseWriter, r *http.Request) { func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) { searchStr := r.URL.Query().Get("search") if searchStr == "" { - exp.StatusPage(w, "search failed", "Empty search string!", NotSupportedStatusType) + exp.StatusPage(w, "search failed", "Empty search string!", ExpStatusNotSupported) return } @@ -1420,7 +1503,7 @@ func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) { return } } - exp.StatusPage(w, "search failed", "Block "+searchStr+" has not yet been mined", NotFoundStatusType) + exp.StatusPage(w, "search failed", "Block "+searchStr+" has not yet been mined", ExpStatusNotFound) return } @@ -1443,7 +1526,7 @@ func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) { // Remaining possibilities are hashes, so verify the string is a hash. if _, err = chainhash.NewHashFromStr(searchStr); err != nil { - exp.StatusPage(w, "search failed", "Search string is not a valid hash or address: "+searchStr, NotFoundStatusType) + exp.StatusPage(w, "search failed", "Search string is not a valid hash or address: "+searchStr, ExpStatusNotFound) return } @@ -1479,15 +1562,15 @@ func (exp *explorerUI) Search(w http.ResponseWriter, r *http.Request) { } } - exp.StatusPage(w, "search failed", "The search string does not match any address, block, or transaction: "+searchStr, NotFoundStatusType) + exp.StatusPage(w, "search failed", "The search string does not match any address, block, or transaction: "+searchStr, ExpStatusNotFound) } // StatusPage provides a page for displaying status messages and exception // handling without redirecting. -func (exp *explorerUI) StatusPage(w http.ResponseWriter, code, message string, sType statusType) { +func (exp *explorerUI) StatusPage(w http.ResponseWriter, code, message string, sType expStatus) { str, err := exp.templates.execTemplateToString("status", struct { *CommonPageData - StatusType statusType + StatusType expStatus Code string Message string NetName string @@ -1505,18 +1588,20 @@ func (exp *explorerUI) StatusPage(w http.ResponseWriter, code, message string, s w.Header().Set("Content-Type", "text/html") switch sType { - case NotFoundStatusType: + case ExpStatusDBTimeout: + w.WriteHeader(http.StatusServiceUnavailable) + case ExpStatusNotFound: w.WriteHeader(http.StatusNotFound) - case FutureBlockStatusType: + case ExpStatusFutureBlock: w.WriteHeader(http.StatusOK) - case ErrorStatusType: + case ExpStatusError: w.WriteHeader(http.StatusInternalServerError) // When blockchain sync is running, status 202 is used to imply that the // other requests apart from serving the status sync page have been received // and accepted but cannot be processed now till the sync is complete. - case BlockchainSyncingType: + case ExpStatusSyncing: w.WriteHeader(http.StatusAccepted) - case NotSupportedStatusType: + case ExpStatusNotSupported: w.WriteHeader(http.StatusUnprocessableEntity) default: w.WriteHeader(http.StatusServiceUnavailable) @@ -1526,7 +1611,7 @@ func (exp *explorerUI) StatusPage(w http.ResponseWriter, code, message string, s // NotFound wraps StatusPage to display a 404 page. func (exp *explorerUI) NotFound(w http.ResponseWriter, r *http.Request) { - exp.StatusPage(w, "Page not found.", "Cannot find page: "+r.URL.Path, NotFoundStatusType) + exp.StatusPage(w, "Page not found.", "Cannot find page: "+r.URL.Path, ExpStatusNotFound) } // ParametersPage is the page handler for the "/parameters" path. @@ -1552,7 +1637,7 @@ func (exp *explorerUI) ParametersPage(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -1564,13 +1649,13 @@ func (exp *explorerUI) ParametersPage(w http.ResponseWriter, r *http.Request) { func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) { if exp.liteMode { exp.StatusPage(w, fullModeRequired, - "Agenda page cannot run in lite mode.", NotSupportedStatusType) + "Agenda page cannot run in lite mode.", ExpStatusNotSupported) return } errPageInvalidAgenda := func(err error) { log.Errorf("Template execute failure: %v", err) exp.StatusPage(w, defaultErrorCode, - "the agenda ID given seems to not exist", NotFoundStatusType) + "the agenda ID given seems to not exist", ExpStatusNotFound) } // Attempt to get agendaid string from URL path. @@ -1582,12 +1667,18 @@ func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) { } chartDataByTime, err := exp.explorerSource.AgendaVotes(agendaid, 0) + if exp.timeoutErrorPage(w, err, "AgendaVotes") { + return + } if err != nil { errPageInvalidAgenda(err) return } chartDataByHeight, err := exp.explorerSource.AgendaVotes(agendaid, 1) + if exp.timeoutErrorPage(w, err, "AgendaVotes") { + return + } if err != nil { errPageInvalidAgenda(err) return @@ -1609,7 +1700,7 @@ func (exp *explorerUI) AgendaPage(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -1622,7 +1713,7 @@ func (exp *explorerUI) AgendasPage(w http.ResponseWriter, r *http.Request) { agendas, err := agendadb.GetAllAgendas() if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } @@ -1638,7 +1729,7 @@ func (exp *explorerUI) AgendasPage(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") @@ -1749,7 +1840,7 @@ func (exp *explorerUI) StatsPage(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ErrorStatusType) + exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, ExpStatusError) return } w.Header().Set("Content-Type", "text/html") diff --git a/explorer/explorertypes.go b/explorer/explorertypes.go index a06125c64..07c631521 100644 --- a/explorer/explorertypes.go +++ b/explorer/explorertypes.go @@ -17,18 +17,19 @@ import ( "github.com/decred/dcrdata/v3/txhelpers" ) -// statusType defines the various status types supported by the system. -type statusType string +// expStatus defines the various status types supported by the system. +type expStatus string const ( - ErrorStatusType statusType = "Error" - NotFoundStatusType statusType = "Not Found" - FutureBlockStatusType statusType = "Future Block" - NotSupportedStatusType statusType = "Not Supported" - NotImplementedStatusType statusType = "Not Implemented" - WrongNetworkStatusType statusType = "Wrong Network" - DeprecatedStatusType statusType = "Deprecated" - BlockchainSyncingType statusType = "Blocks Syncing" + ExpStatusError expStatus = "Error" + ExpStatusNotFound expStatus = "Not Found" + ExpStatusFutureBlock expStatus = "Future Block" + ExpStatusNotSupported expStatus = "Not Supported" + ExpStatusNotImplemented expStatus = "Not Implemented" + ExpStatusWrongNetwork expStatus = "Wrong Network" + ExpStatusDeprecated expStatus = "Deprecated" + ExpStatusSyncing expStatus = "Blocks Syncing" + ExpStatusDBTimeout expStatus = "Database Timeout" ) // blockchainSyncStatus defines the status update displayed on the syncing status page diff --git a/explorer/websockethandlers.go b/explorer/websockethandlers.go index b3f050116..82760c911 100644 --- a/explorer/websockethandlers.go +++ b/explorer/websockethandlers.go @@ -121,6 +121,11 @@ func (exp *explorerUI) RootWebsocket(w http.ResponseWriter, r *http.Request) { // although it is automatically updated by the first caller // who requests data from a stale cache. cData, gData, chartHeight, err := exp.explorerSource.TicketPoolVisualization(interval) + if dbtypes.IsTimeoutErr(err) { + log.Warnf("TicketPoolVisualization DB timeout: %v", err) + webData.Message = "Error: DB timeout" + break + } if err != nil { if strings.HasPrefix(err.Error(), "unknown interval") { log.Debugf("invalid ticket pool interval provided "+ diff --git a/main.go b/main.go index dbd4e4dee..74d1688ab 100644 --- a/main.go +++ b/main.go @@ -165,11 +165,12 @@ func _main(ctx context.Context) error { } } dbi := dcrpg.DBInfo{ - Host: pgHost, - Port: pgPort, - User: cfg.PGUser, - Pass: cfg.PGPass, - DBName: cfg.PGDBName, + Host: pgHost, + Port: pgPort, + User: cfg.PGUser, + Pass: cfg.PGPass, + DBName: cfg.PGDBName, + QueryTimeout: cfg.PGQueryTimeout, } chainDB, err := dcrpg.NewChainDBWithCancel(ctx, &dbi, activeChain, baseDB.GetStakeDB(), !cfg.NoDevPrefetch) if chainDB != nil { diff --git a/public/js/controllers/address.js b/public/js/controllers/address.js index 128471083..6fe4227c6 100644 --- a/public/js/controllers/address.js +++ b/public/js/controllers/address.js @@ -310,10 +310,10 @@ var pastDay = d.getDate() - 1 this.enabledButtons = [] - var setApplicableBtns = (className, ts, txCountByType) => { + var setApplicableBtns = (className, ts, numIntervals) => { var isDisabled = (val > new Date(ts)) || (this.options === 'unspent' && this.unspent == "0") || - txCountByType < 2; + numIntervals < 2; if (isDisabled) { this.zoomTarget.getElementsByClassName(className)[0].setAttribute("disabled", isDisabled)