From e1e81d94590f7bcfb8e5e6cc28db77de3634eb75 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Tue, 3 Nov 2020 10:43:25 +0000 Subject: [PATCH 1/5] Postgres implementation. This commit adds a full postgres database implementation for dcrpool. - New config items to enable postgres and set database connection details. - Updated documentation to explain postgres usage. - Database backup features are disabled when using postgres. - All database tests are now run against both bolt and postgres database. --- README.md | 17 +- config.go | 18 + dcrpool.go | 27 +- docs/postgres.md | 40 ++ go.mod | 1 + go.sum | 2 + gui/admin.go | 2 + gui/assets/templates/admin.html | 3 +- harness.sh | 15 + pool/acceptedwork.go | 4 +- pool/boltdb.go | 1 + pool/database.go | 6 + pool/pool_test.go | 81 ++- pool/postgres.go | 948 ++++++++++++++++++++++++++++++++ pool/share.go | 3 +- 15 files changed, 1132 insertions(+), 36 deletions(-) create mode 100644 docs/postgres.md create mode 100644 pool/postgres.go diff --git a/README.md b/README.md index fd80d819..3cdf5d99 100644 --- a/README.md +++ b/README.md @@ -41,17 +41,13 @@ of the address mining rewards are paid to and its name, formatted as: the address provided in the username to create an account, all other connected miners with the same address set will contribute work to that account. -As a contingency, the pool maintains a backup of the database (`backup.kv`), -created on shutdown in the same directory as the database itself. - The user interface of the pool provides public access to statistics and pool account data. Users of the pool can access all payments, mined blocks by the account and also work contributed by clients of the account via the interface. The interface is only accessible via HTTPS and by default uses a self-signed certificate, served on port `:8080`. In production, particularly for pool mining, a certificate from an authority (`CA`) like -[letsencrypt](https://letsencrypt.org/) is recommended. The user interface also -provides pool administrators database backup functionality when needed. +[letsencrypt](https://letsencrypt.org/) is recommended. ## Installing and Updating @@ -75,6 +71,17 @@ run `go install . ./cmd/...` in the root directory. Some notes: - The `dcrpool` executable will be installed to `$GOPATH/bin`. `GOPATH` defaults to `$HOME/go` (or `%USERPROFILE%\go` on Windows) if unset. +## Database + +dcrpool can run with either a [Bolt database](https://github.com/etcd-io/bbolt) +or a [Postgres database](https://www.postgresql.org/). Bolt is used by default. +[postgres.md](./docs/postgres.md) has more details about running with Postgres. + +When running in Bolt mode, the pool maintains a backup of the database +(`backup.kv`), created on shutdown in the same directory as the database itself. +The user interface also provides functionality for pool administrators to backup +Bolt database when necessary. + ### Example of obtaining and building from source on Ubuntu ```sh diff --git a/config.go b/config.go index dad64787..2bfe7a22 100644 --- a/config.go +++ b/config.go @@ -58,6 +58,12 @@ const ( defaultMaxConnectionsPerHost = 100 // 100 connected clients per host defaultWalletAccount = 0 defaultCoinbaseConfTimeout = time.Minute * 5 // one block time + defaultUsePostgres = false + defaultPGHost = "127.0.0.1" + defaultPGPort = 5432 + defaultPGUser = "dcrpooluser" + defaultPGPass = "12345" + defaultPGDBName = "dcrpooldb" ) var ( @@ -128,6 +134,12 @@ type config struct { DCR1Port uint32 `long:"dcr1port" ini-name:"dcr1port" description:"Obelisk DCR1 connection port."` CoinbaseConfTimeout time.Duration `long:"conftimeout" ini-name:"conftimeout" description:"The duration to wait for coinbase confirmations."` GenCertsOnly bool `long:"gencertsonly" ini-name:"gencertsonly" description:"Only generate needed TLS key pairs and terminate."` + UsePostgres bool `long:"postgres" ini-name:"postgres" description:"Use postgres database instead of bolt."` + PGHost string `long:"postgreshost" ini-name:"postgreshost" description:"Host to establish a postgres connection."` + PGPort uint32 `long:"postgresport" ini-name:"postgresport" description:"Port to establish a postgres connection."` + PGUser string `long:"postgresuser" ini-name:"postgresuser" description:"Username for postgres authentication."` + PGPass string `long:"postgrespass" ini-name:"postgrespass" description:"Password for postgres authentication."` + PGDBName string `long:"postgresdbname" ini-name:"postgresdbname" description:"Postgres database name."` poolFeeAddrs []dcrutil.Address dcrdRPCCerts []byte net *params @@ -362,6 +374,12 @@ func loadConfig() (*config, []string, error) { DCR1Port: defaultDCR1Port, WalletAccount: defaultWalletAccount, CoinbaseConfTimeout: defaultCoinbaseConfTimeout, + UsePostgres: defaultUsePostgres, + PGHost: defaultPGHost, + PGPort: defaultPGPort, + PGUser: defaultPGUser, + PGPass: defaultPGPass, + PGDBName: defaultPGDBName, } // Service options which are only added on Windows. diff --git a/dcrpool.go b/dcrpool.go index b6d94835..30c6c10c 100644 --- a/dcrpool.go +++ b/dcrpool.go @@ -212,13 +212,17 @@ func newPool(db pool.Database, cfg *config) (*miningPool, error) { FetchLastPaymentInfo: p.hub.FetchLastPaymentInfo, FetchMinedWork: p.hub.FetchMinedWork, FetchWorkQuotas: p.hub.FetchWorkQuotas, - HTTPBackupDB: p.hub.HTTPBackupDB, FetchClients: p.hub.FetchClients, AccountExists: p.hub.AccountExists, FetchArchivedPayments: p.hub.FetchArchivedPayments, FetchPendingPayments: p.hub.FetchPendingPayments, FetchCacheChannel: p.hub.FetchCacheChannel, } + + if !cfg.UsePostgres { + gcfg.HTTPBackupDB = p.hub.HTTPBackupDB + } + p.gui, err = gui.NewGUI(gcfg) if err != nil { p.hub.CloseListeners() @@ -245,7 +249,14 @@ func main() { } }() - db, err := pool.InitBoltDB(cfg.DBFile) + var db pool.Database + if cfg.UsePostgres { + db, err = pool.InitPostgresDB(cfg.PGHost, cfg.PGPort, cfg.PGUser, + cfg.PGPass, cfg.PGDBName) + } else { + db, err = pool.InitBoltDB(cfg.DBFile) + } + if err != nil { mpLog.Errorf("failed to initialize database: %v", err) os.Exit(1) @@ -292,11 +303,13 @@ func main() { p.hub.Run(p.ctx) // hub.Run() blocks until the pool is fully shut down. When it returns, - // write a backup of the DB, and then close the DB. - mpLog.Tracef("Backing up database.") - err = db.Backup(pool.BoltBackupFile) - if err != nil { - mpLog.Errorf("failed to write database backup file: %v", err) + // write a backup of the DB (if not using postgres), and then close the DB. + if !cfg.UsePostgres { + mpLog.Tracef("Backing up database.") + err = db.Backup(pool.BoltBackupFile) + if err != nil { + mpLog.Errorf("failed to write database backup file: %v", err) + } } db.Close() diff --git a/docs/postgres.md b/docs/postgres.md new file mode 100644 index 00000000..dff07871 --- /dev/null +++ b/docs/postgres.md @@ -0,0 +1,40 @@ +# Running dcrpool with PostgreSQL + +Tested with PostgreSQL 13.0. + +**Note:** When running in Postgres mode, backups will not be created +automatically by dcrpool. + +1. Connect to your instance of PostgreSQL using `psql` to create a new database + and a new user for dcrpool. + Be sure to substitute the example password `12345` with something more secure. + + ```no-highlight + postgres=# CREATE DATABASE dcrpooldb; + CREATE DATABASE + postgres=# CREATE USER dcrpooluser WITH ENCRYPTED PASSWORD '12345'; + CREATE ROLE + postgres=# GRANT ALL PRIVILEGES ON DATABASE dcrpooldb to dcrpooluser; + GRANT + ``` + +1. **Developers only** - if you are modifying code and wish to run the dcrpool + test suite, you will need to create an additional database. + + ```no-highlight + postgres=# CREATE DATABASE dcrpooltestdb; + CREATE DATABASE + postgres=# GRANT ALL PRIVILEGES ON DATABASE dcrpooltestdb to dcrpooluser; + GRANT + ``` + +1. Add the database connection details to the dcrpool config file. + + ```no-highlight + postgres=true + postgreshost=127.0.0.1 + postgresport=5432 + postgresuser=dcrpooluser + postgrespass=12345 + postgresdbname=dcrpooldb + ``` diff --git a/go.mod b/go.mod index 8cf2be12..c2951b6b 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7 github.com/jrick/logrotate v1.0.0 github.com/kr/pretty v0.1.0 // indirect + github.com/lib/pq v1.8.0 go.etcd.io/bbolt v1.3.5 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 diff --git a/go.sum b/go.sum index e194e323..d17410fc 100644 --- a/go.sum +++ b/go.sum @@ -115,6 +115,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= diff --git a/gui/admin.go b/gui/admin.go index 8e8f80f2..0c9b8efc 100644 --- a/gui/admin.go +++ b/gui/admin.go @@ -22,6 +22,7 @@ type adminPageData struct { ArchivedPayments []*archivedPayment PendingPaymentsTotal string PendingPayments []*pendingPayment + BackupAvailable bool } // adminPage is the handler for "GET /admin". If the current session is @@ -73,6 +74,7 @@ func (ui *GUI) adminPage(w http.ResponseWriter, r *http.Request) { PendingPayments: pendingPmts, ArchivedPaymentsTotal: totalArchived, ArchivedPayments: archivedPmts, + BackupAvailable: ui.cfg.HTTPBackupDB != nil, } ui.renderTemplate(w, "admin", pageData) diff --git a/gui/assets/templates/admin.html b/gui/assets/templates/admin.html index 53e90f6b..e57ee1a6 100644 --- a/gui/assets/templates/admin.html +++ b/gui/assets/templates/admin.html @@ -9,11 +9,12 @@

Admin Panel

+ {{ if .BackupAvailable }}
{{.HeaderData.CSRF}}
- + {{ end }}
{{.HeaderData.CSRF}} diff --git a/harness.sh b/harness.sh index ea3f5ff1..b94d520d 100755 --- a/harness.sh +++ b/harness.sh @@ -29,6 +29,15 @@ MINER_MAX_PROCS=1 PAYMENT_METHOD="pplns" LAST_N_PERIOD=5m GUI_DIR="${HARNESS_ROOT}/gui" + +# Using postgres requires the DB specified below to exist and contain no data. +USE_POSTGRES=true +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_USER=dcrpooluser +POSTGRES_PASS=12345 +POSTGRES_DBNAME=dcrpooldb + # CPU_MINING_ADDR is the mining address printed during creation of vwallet. # Initial block rewards from `generate` are sent here so vwallet can buy tickets. CPU_MINING_ADDR="SsaJxXSymEGroxAiUY9u1mRq1DDWLxn5WhB" @@ -128,6 +137,12 @@ adminpass=${ADMIN_PASS} guidir=${GUI_DIR} designation=${TMUX_SESSION} profile=6060 +postgres=${USE_POSTGRES} +postgreshost=${POSTGRES_HOST} +postgresport=${POSTGRES_PORT} +postgresuser=${POSTGRES_USER} +postgrespass=${POSTGRES_PASS} +postgresdbname=${POSTGRES_DBNAME} EOF cat > "${HARNESS_ROOT}/mwallet/dcrmwctl.conf" <(estimatedmaturity+1);` + + rows, err := db.DB.Query(stmt, height) + if err != nil { + return nil, err + } + + return decodePaymentRows(rows) +} + +// fetchPendingPayments fetches all unpaid payments. +func (db *PostgresDB) fetchPendingPayments() ([]*Payment, error) { + const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, + paidonheight, transactionid, sourceblockhash, sourcecoinbase + FROM payments + WHERE paidonheight=0;` + + rows, err := db.DB.Query(stmt) + if err != nil { + return nil, err + } + + return decodePaymentRows(rows) +} + +// pendingPaymentsForBlockHash returns the number of pending payments with the +// provided block hash as their source. +func (db *PostgresDB) pendingPaymentsForBlockHash(blockHash string) (uint32, error) { + const stmt = `SELECT count(1) + FROM payments + WHERE paidonheight=0 + AND sourceblockhash=$1;` + + var count uint32 + err := db.DB.QueryRow(stmt, blockHash).Scan(&count) + if err != nil { + return 0, err + } + + return count, nil +} + +// archivedPayments fetches all archived payments. List is ordered, most +// recent comes first. +func (db *PostgresDB) archivedPayments() ([]*Payment, error) { + const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, + paidonheight, transactionid, sourceblockhash, sourcecoinbase + FROM archivedpayments + ORDER BY height DESC;` + + rows, err := db.DB.Query(stmt) + if err != nil { + return nil, err + } + + return decodePaymentRows(rows) +} + +// maturePendingPayments fetches all mature pending payments at the +// provided height. +func (db *PostgresDB) maturePendingPayments(height uint32) (map[string][]*Payment, error) { + const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, + paidonheight, transactionid, sourceblockhash, sourcecoinbase + FROM payments + WHERE paidonheight=0 + AND (estimatedmaturity+1)<=$1;` + + rows, err := db.DB.Query(stmt, height) + if err != nil { + return nil, err + } + + payments, err := decodePaymentRows(rows) + if err != nil { + return nil, err + } + + pmts := make(map[string][]*Payment) + for _, pmt := range payments { + set, ok := pmts[pmt.Source.BlockHash] + if !ok { + set = make([]*Payment, 0) + } + set = append(set, pmt) + pmts[pmt.Source.BlockHash] = set + } + + return pmts, nil +} + +// fetchShare fetches the share referenced by the provided id. Returns an error +// if the share is not found. +func (db *PostgresDB) fetchShare(id string) (*Share, error) { + const funcName = "fetchShare" + const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE uuid=$1;` + var uuid, account, weight string + var createdOn int64 + err := db.DB.QueryRow(stmt, id).Scan(&uuid, &account, &weight, &createdOn) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + desc := fmt.Sprintf("%s: no share found for id %s", funcName, id) + return nil, dbError(ErrValueNotFound, desc) + } + + return nil, err + } + + weightRat, ok := new(big.Rat).SetString(weight) + if !ok { + desc := fmt.Sprintf("%s: unable to decode rat string: %v", + funcName, err) + return nil, dbError(ErrParse, desc) + } + + return &Share{uuid, account, weightRat, createdOn}, nil +} + +// PersistShare saves a share to the database. Returns an error if a share +// already exists with the same ID. +func (db *PostgresDB) PersistShare(share *Share) error { + const funcName = "PersistShare" + const stmt = `INSERT INTO shares(uuid, account, weight, createdon) VALUES ($1,$2,$3,$4);` + _, err := db.DB.Exec(stmt, share.UUID, share.Account, share.Weight.RatString(), share.CreatedOn) + if err != nil { + + var pqError *pq.Error + if errors.As(err, &pqError) { + if pqError.Code.Name() == "unique_violation" { + desc := fmt.Sprintf("%s: share %s already exists", funcName, + share.UUID) + return dbError(ErrValueFound, desc) + } + } + + return err + } + return nil +} + +// ppsEligibleShares fetches all shares created before or at the provided time. +func (db *PostgresDB) ppsEligibleShares(max int64) ([]*Share, error) { + const funcName = "ppsEligibleShares" + const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE createdon <= $1` + rows, err := db.DB.Query(stmt, max) + if err != nil { + return nil, err + } + + return decodeShareRows(rows) +} + +// pplnsEligibleShares fetches all shares created after the provided time. +func (db *PostgresDB) pplnsEligibleShares(min int64) ([]*Share, error) { + const funcName = "pplnsEligibleShares" + const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE createdon > $1` + rows, err := db.DB.Query(stmt, min) + if err != nil { + return nil, err + } + + return decodeShareRows(rows) +} + +// pruneShares removes shares with a createdOn time earlier than the provided +// time. +func (db *PostgresDB) pruneShares(minNano int64) error { + const stmt = `DELETE FROM shares WHERE createdon < $1` + _, err := db.DB.Exec(stmt, minNano) + return err +} + +// fetchAcceptedWork fetches the accepted work referenced by the provided id. +// Returns an error if the work is not found. +func (db *PostgresDB) fetchAcceptedWork(id string) (*AcceptedWork, error) { + const funcName = "fetchAcceptedWork" + const stmt = `SELECT + uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed + FROM acceptedwork WHERE uuid=$1;` + + var uuid, blockhash, prevhash, minedby, miner string + var confirmed bool + var height uint32 + var createdOn int64 + err := db.DB.QueryRow(stmt, id).Scan(&uuid, &blockhash, &prevhash, &height, + &minedby, &miner, &createdOn, &confirmed) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + desc := fmt.Sprintf("%s: no work found for id %s", funcName, id) + return nil, dbError(ErrValueNotFound, desc) + } + + return nil, err + } + + return &AcceptedWork{uuid, blockhash, prevhash, height, + minedby, miner, createdOn, confirmed}, nil +} + +// persistAcceptedWork saves the accepted work to the database. +func (db *PostgresDB) persistAcceptedWork(work *AcceptedWork) error { + const funcName = "persistAcceptedWork" + + const stmt = `INSERT INTO acceptedwork( + uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8);` + _, err := db.DB.Exec(stmt, work.UUID, work.BlockHash, work.PrevHash, + work.Height, work.MinedBy, work.Miner, work.CreatedOn, work.Confirmed) + if err != nil { + + var pqError *pq.Error + if errors.As(err, &pqError) { + if pqError.Code.Name() == "unique_violation" { + desc := fmt.Sprintf("%s: work %s already exists", funcName, + work.UUID) + return dbError(ErrValueFound, desc) + } + } + + return err + } + return nil +} + +// updateAcceptedWork persists modifications to an existing work. Returns an +// error if the work is not found. +func (db *PostgresDB) updateAcceptedWork(work *AcceptedWork) error { + const funcName = "updateAcceptedWork" + const stmt = `UPDATE acceptedwork SET + blockhash=$2, + prevhash=$3, + height=$4, + minedby=$5, + miner=$6, + createdon=$7, + confirmed=$8 + WHERE uuid=$1;` + result, err := db.DB.Exec(stmt, + work.UUID, work.BlockHash, work.PrevHash, + work.Height, work.MinedBy, work.Miner, work.CreatedOn, work.Confirmed) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + desc := fmt.Sprintf("%s: work %s not found", funcName, work.UUID) + return dbError(ErrValueNotFound, desc) + } + + return nil +} + +// deleteAcceptedWork removes the associated accepted work from the database. +func (db *PostgresDB) deleteAcceptedWork(id string) error { + const stmt = `DELETE FROM acceptedwork WHERE uuid=$1;` + _, err := db.DB.Exec(stmt, id) + return err +} + +// listMinedWork returns work data associated with all blocks mined by the pool +// regardless of whether they are confirmed or not. +// +// List is ordered, most recent comes first. +func (db *PostgresDB) listMinedWork() ([]*AcceptedWork, error) { + const stmt = `SELECT + uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed + FROM acceptedwork + ORDER BY height DESC;` + + rows, err := db.DB.Query(stmt) + if err != nil { + return nil, err + } + + return decodeWorkRows(rows) + +} + +// fetchUnconfirmedWork returns all work which is not confirmed as mined with +// height less than the provided height. +func (db *PostgresDB) fetchUnconfirmedWork(height uint32) ([]*AcceptedWork, error) { + const stmt = `SELECT + uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed + FROM acceptedwork + WHERE $1>height + AND confirmed=false;` + + rows, err := db.DB.Query(stmt, height) + if err != nil { + return nil, err + } + + return decodeWorkRows(rows) +} + +// fetchJob fetches the job referenced by the provided id. Returns an error if +// the job is not found. +func (db *PostgresDB) fetchJob(id string) (*Job, error) { + const funcName = "fetchJob" + const stmt = `SELECT uuid, header, height FROM jobs WHERE uuid=$1;` + var uuid, header string + var height uint32 + err := db.DB.QueryRow(stmt, id).Scan(&uuid, &header, &height) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + desc := fmt.Sprintf("%s: no job found for id %s", funcName, id) + return nil, dbError(ErrValueNotFound, desc) + } + + return nil, err + } + return &Job{uuid, height, header}, nil +} + +// persistJob saves the job to the database. Returns an error if an account +// already exists with the same ID. +func (db *PostgresDB) persistJob(job *Job) error { + const funcName = "persistJob" + const stmt = `INSERT INTO jobs(uuid, height, header) VALUES ($1,$2,$3);` + _, err := db.DB.Exec(stmt, job.UUID, job.Height, job.Header) + if err != nil { + + var pqError *pq.Error + if errors.As(err, &pqError) { + if pqError.Code.Name() == "unique_violation" { + desc := fmt.Sprintf("%s: job %s already exists", funcName, + job.UUID) + return dbError(ErrValueFound, desc) + } + } + + return err + } + return nil +} + +// deleteJob removes the associated job from the database. +func (db *PostgresDB) deleteJob(id string) error { + const stmt = `DELETE FROM jobs WHERE uuid=$1;` + _, err := db.DB.Exec(stmt, id) + return err +} + +// deleteJobsBeforeHeight removes all jobs with heights less than the provided +// height. +func (db *PostgresDB) deleteJobsBeforeHeight(height uint32) error { + const stmt = `DELETE FROM jobs WHERE height < $1;` + _, err := db.DB.Exec(stmt, height) + return err +} diff --git a/pool/share.go b/pool/share.go index e34eba10..9429b84f 100644 --- a/pool/share.go +++ b/pool/share.go @@ -158,8 +158,7 @@ func (db *BoltDB) ppsEligibleShares(max int64) ([]*Share, error) { return eligibleShares, err } -// pplnsEligibleShares fetches all shares keyed greater than the provided -// minimum. +// pplnsEligibleShares fetches all shares created after the provided time. func (db *BoltDB) pplnsEligibleShares(min int64) ([]*Share, error) { funcName := "pplnsEligibleShares" eligibleShares := make([]*Share, 0) From 9cf90cdd06f784e5f4a9bf90014f24564570a429 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Tue, 3 Nov 2020 15:16:45 +0000 Subject: [PATCH 2/5] ci: Add postgres to GitHub Actions --- .github/workflows/go.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 19a57db9..2cd97d43 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,6 +7,21 @@ jobs: strategy: matrix: go: [1.14, 1.15] + + services: + postgres: + image: postgres:13.0 + env: + POSTGRES_USER: dcrpooluser + POSTGRES_PASSWORD: 12345 + POSTGRES_DB: dcrpooltestdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - name: Set up Go uses: actions/setup-go@v2 From e6b5bcf45eb71e4f1c0a6ecc7f36d12b5f67394a Mon Sep 17 00:00:00 2001 From: jholdstock Date: Tue, 3 Nov 2020 15:36:32 +0000 Subject: [PATCH 3/5] docs: Mention PGTune --- docs/postgres.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/postgres.md b/docs/postgres.md index dff07871..343e62c6 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -5,6 +5,8 @@ Tested with PostgreSQL 13.0. **Note:** When running in Postgres mode, backups will not be created automatically by dcrpool. +## Setup + 1. Connect to your instance of PostgreSQL using `psql` to create a new database and a new user for dcrpool. Be sure to substitute the example password `12345` with something more secure. @@ -38,3 +40,10 @@ automatically by dcrpool. postgrespass=12345 postgresdbname=dcrpooldb ``` + +## Tuning + +A helpful online tool to determine good settings for your system is called +[PGTune](https://pgtune.leopard.in.ua/#/). After providing basic information +about your hardware, PGTune will output a snippet of optimization settings to +add to your PostgreSQL config. From e34697a8915546be672eecaf3c7edc955714bc78 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 5 Nov 2020 10:27:46 +0000 Subject: [PATCH 4/5] Move queries into their own file. --- pool/postgres.go | 315 ++++++------------------------------- pool/sql_queries.go | 372 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 265 deletions(-) create mode 100644 pool/sql_queries.go diff --git a/pool/postgres.go b/pool/postgres.go index 21abe5e8..8092ddb6 100644 --- a/pool/postgres.go +++ b/pool/postgres.go @@ -42,37 +42,37 @@ func InitPostgresDB(host string, port uint32, user, pass, dbName string) (*Postg return nil, dbError(ErrDBOpen, desc) } - err = createMetadataTable(db) + _, err = db.Exec(createTableMetadata) if err != nil { return nil, err } - err = createAccountsTable(db) + _, err = db.Exec(createTableAccounts) if err != nil { return nil, err } - err = createJobsTable(db) + _, err = db.Exec(createTablePayments) if err != nil { return nil, err } - err = createSharesTable(db) + _, err = db.Exec(createTableArchivedPayments) if err != nil { return nil, err } - err = createPaymentsTable(db) + _, err = db.Exec(createTableJobs) if err != nil { return nil, err } - err = createArchivedPaymentsTable(db) + _, err = db.Exec(createTableShares) if err != nil { return nil, err } - err = createAcceptedWorkTable(db) + _, err = db.Exec(createTableAcceptedWork) if err != nil { return nil, err } @@ -85,102 +85,6 @@ func (db *PostgresDB) Close() error { return db.DB.Close() } -func createMetadataTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createAccountsTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS accounts ( - uuid TEXT PRIMARY KEY, - address TEXT NOT NULL, - createdon INT8 NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createPaymentsTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS payments ( - uuid TEXT PRIMARY KEY, - account TEXT NOT NULL, - estimatedmaturity INT8 NOT NULL, - height INT8 NOT NULL, - amount INT8 NOT NULL, - createdon INT8 NOT NULL, - paidonheight INT8 NOT NULL, - transactionid TEXT NOT NULL, - sourceblockhash TEXT NOT NULL, - sourcecoinbase TEXT NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createArchivedPaymentsTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS archivedpayments ( - uuid TEXT PRIMARY KEY, - account TEXT NOT NULL, - estimatedmaturity INT8 NOT NULL, - height INT8 NOT NULL, - amount INT8 NOT NULL, - createdon INT8 NOT NULL, - paidonheight INT8 NOT NULL, - transactionid TEXT NOT NULL, - sourceblockhash TEXT NOT NULL, - sourcecoinbase TEXT NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createJobsTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS jobs ( - uuid TEXT PRIMARY KEY, - height INT8 NOT NULL, - header TEXT NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createSharesTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS shares ( - uuid TEXT PRIMARY KEY, - account TEXT NOT NULL, - weight TEXT NOT NULL, - createdon INT8 NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - -func createAcceptedWorkTable(db *sql.DB) error { - const stmt = `CREATE TABLE IF NOT EXISTS acceptedwork ( - uuid TEXT PRIMARY KEY, - blockhash TEXT NOT NULL, - prevhash TEXT NOT NULL, - height INT8 NOT NULL, - minedby TEXT NOT NULL, - miner TEXT NOT NULL, - createdon INT8 NOT NULL, - confirmed BOOLEAN NOT NULL - );` - - _, err := db.Exec(stmt) - return err -} - func decodePaymentRows(rows *sql.Rows) ([]*Payment, error) { var toReturn []*Payment for rows.Next() { @@ -273,9 +177,8 @@ func (db *PostgresDB) Backup(fileName string) error { func (db *PostgresDB) fetchPoolMode() (uint32, error) { const funcName = "fetchPoolMode" - const stmt = `SELECT value FROM metadata WHERE key='poolmode';` var poolmode uint32 - err := db.DB.QueryRow(stmt).Scan(&poolmode) + err := db.DB.QueryRow(selectPoolMode).Scan(&poolmode) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no value found for poolmode", funcName) @@ -288,20 +191,14 @@ func (db *PostgresDB) fetchPoolMode() (uint32, error) { } func (db *PostgresDB) persistPoolMode(mode uint32) error { - const stmt = `INSERT INTO metadata(key, value) - VALUES ('poolmode', $1) - ON CONFLICT (key) - DO UPDATE SET value=$1;` - - _, err := db.DB.Exec(stmt, mode) + _, err := db.DB.Exec(insertPoolMode, mode) return err } func (db *PostgresDB) fetchCSRFSecret() ([]byte, error) { const funcName = "fetchCSRFSecret" - const stmt = `SELECT value FROM metadata WHERE key='csrfsecret';` var secret string - err := db.DB.QueryRow(stmt).Scan(&secret) + err := db.DB.QueryRow(selectCSRFSecret).Scan(&secret) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no value found for csrfsecret", funcName) @@ -322,12 +219,7 @@ func (db *PostgresDB) fetchCSRFSecret() ([]byte, error) { } func (db *PostgresDB) persistCSRFSecret(secret []byte) error { - const stmt = `INSERT INTO metadata(key, value) - VALUES ('csrfsecret', $1) - ON CONFLICT (key) - DO UPDATE SET value=$1;` - - _, err := db.DB.Exec(stmt, hex.EncodeToString(secret)) + _, err := db.DB.Exec(insertCSRFSecret, hex.EncodeToString(secret)) return err } @@ -337,23 +229,13 @@ func (db *PostgresDB) persistLastPaymentInfo(height uint32, paidOn int64) error return err } - const stmt = `INSERT INTO metadata(key, value) - VALUES ('lastpaymentheight', $1) - ON CONFLICT (key) - DO UPDATE SET value=$1;` - - _, err = tx.Exec(stmt, height) + _, err = tx.Exec(insertLastPaymentHeight, height) if err != nil { tx.Rollback() return err } - const stmt2 = `INSERT INTO metadata(key, value) - VALUES ('lastpaymentpaidon', $1) - ON CONFLICT (key) - DO UPDATE SET value=$1;` - - _, err = tx.Exec(stmt2, paidOn) + _, err = tx.Exec(insertLastPaymentPaidOn, paidOn) if err != nil { tx.Rollback() return err @@ -364,9 +246,9 @@ func (db *PostgresDB) persistLastPaymentInfo(height uint32, paidOn int64) error func (db *PostgresDB) loadLastPaymentInfo() (uint32, int64, error) { const funcName = "loadLastPaymentInfo" - const stmt = `SELECT value FROM metadata WHERE key='lastpaymentheight';` + var height uint32 - err := db.DB.QueryRow(stmt).Scan(&height) + err := db.DB.QueryRow(selectLastPaymentHeight).Scan(&height) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no value found for lastpaymentheight", @@ -377,9 +259,8 @@ func (db *PostgresDB) loadLastPaymentInfo() (uint32, int64, error) { return 0, 0, err } - const stmt2 = `SELECT value FROM metadata WHERE key='lastpaymentpaidon';` var paidOn int64 - err = db.DB.QueryRow(stmt2).Scan(&paidOn) + err = db.DB.QueryRow(selectLastPaymentPaidOn).Scan(&paidOn) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no value found for lastpaymentpaidon", @@ -394,20 +275,14 @@ func (db *PostgresDB) loadLastPaymentInfo() (uint32, int64, error) { } func (db *PostgresDB) persistLastPaymentCreatedOn(createdOn int64) error { - const stmt = `INSERT INTO metadata(key, value) - VALUES ('lastpaymentcreatedon', $1) - ON CONFLICT (key) - DO UPDATE SET value=$1;` - - _, err := db.DB.Exec(stmt, createdOn) + _, err := db.DB.Exec(insertLastPaymentCreatedOn, createdOn) return err } func (db *PostgresDB) loadLastPaymentCreatedOn() (int64, error) { const funcName = "loadLastPaymentCreatedOn" - const stmt = `SELECT value FROM metadata WHERE key='lastpaymentcreatedon';` var createdOn int64 - err := db.DB.QueryRow(stmt).Scan(&createdOn) + err := db.DB.QueryRow(selectLastPaymentCreatedOn).Scan(&createdOn) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no value found for lastpaymentcreatedon", @@ -425,8 +300,7 @@ func (db *PostgresDB) loadLastPaymentCreatedOn() (int64, error) { // already exists with the same ID. func (db *PostgresDB) persistAccount(acc *Account) error { const funcName = "persistAccount" - const stmt = `INSERT INTO accounts(uuid, address, createdon) VALUES ($1,$2,$3);` - _, err := db.DB.Exec(stmt, acc.UUID, acc.Address, uint64(time.Now().Unix())) + _, err := db.DB.Exec(insertAccount, acc.UUID, acc.Address, uint64(time.Now().Unix())) if err != nil { var pqError *pq.Error @@ -447,10 +321,9 @@ func (db *PostgresDB) persistAccount(acc *Account) error { // an error if the account is not found. func (db *PostgresDB) fetchAccount(id string) (*Account, error) { const funcName = "fetchAccount" - const stmt = `SELECT uuid, address, createdon FROM accounts WHERE uuid=$1;` var uuid, address string var createdOn uint64 - err := db.DB.QueryRow(stmt, id).Scan(&uuid, &address, &createdOn) + err := db.DB.QueryRow(selectAccount, id).Scan(&uuid, &address, &createdOn) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no account found for id %s", funcName, id) @@ -464,8 +337,7 @@ func (db *PostgresDB) fetchAccount(id string) (*Account, error) { // deleteAccount purges the referenced account from the database. func (db *PostgresDB) deleteAccount(id string) error { - const stmt = `DELETE FROM accounts WHERE uuid=$1;` - _, err := db.DB.Exec(stmt, id) + _, err := db.DB.Exec(deleteAccount, id) return err } @@ -473,14 +345,11 @@ func (db *PostgresDB) deleteAccount(id string) error { // error if the payment is not found. func (db *PostgresDB) fetchPayment(id string) (*Payment, error) { const funcName = "fetchPayment" - const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - FROM payments WHERE uuid=$1;` var uuid, account, transactionID, sourceBlockHash, sourceCoinbase string var estimatedMaturity, height, paidOnHeight uint32 var amount, createdOn int64 - err := db.DB.QueryRow(stmt, id).Scan(&uuid, &account, &estimatedMaturity, + err := db.DB.QueryRow(selectPayment, id).Scan(&uuid, &account, &estimatedMaturity, &height, &amount, &createdOn, &paidOnHeight, &transactionID, &sourceBlockHash, &sourceCoinbase) if err != nil { @@ -500,11 +369,7 @@ func (db *PostgresDB) fetchPayment(id string) (*Payment, error) { func (db *PostgresDB) PersistPayment(p *Payment) error { const funcName = "PersistPayment" - const stmt = `INSERT INTO payments( - uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);` - _, err := db.DB.Exec(stmt, + _, err := db.DB.Exec(insertPayment, p.UUID, p.Account, p.EstimatedMaturity, p.Height, p.Amount, p.CreatedOn, p.PaidOnHeight, p.TransactionID, p.Source.BlockHash, p.Source.Coinbase) if err != nil { @@ -525,18 +390,7 @@ func (db *PostgresDB) PersistPayment(p *Payment) error { // updatePayment persists the updated payment to the database. func (db *PostgresDB) updatePayment(p *Payment) error { - const stmt = `UPDATE payments SET - account=$2, - estimatedmaturity=$3, - height=$4, - amount=$5, - createdon=$6, - paidonheight=$7, - transactionid=$8, - sourceblockhash=$9, - sourcecoinbase=$10 - WHERE uuid=$1;` - _, err := db.DB.Exec(stmt, + _, err := db.DB.Exec(updatePayment, p.UUID, p.Account, p.EstimatedMaturity, p.Height, p.Amount, p.CreatedOn, p.PaidOnHeight, p.TransactionID, p.Source.BlockHash, p.Source.Coinbase) return err @@ -545,8 +399,7 @@ func (db *PostgresDB) updatePayment(p *Payment) error { // deletePayment purges the referenced payment from the database. Note that // archived payments cannot be deleted. func (db *PostgresDB) deletePayment(id string) error { - const stmt = `DELETE FROM payments WHERE uuid=$1;` - _, err := db.DB.Exec(stmt, id) + _, err := db.DB.Exec(deletePayment, id) return err } @@ -559,8 +412,7 @@ func (db *PostgresDB) ArchivePayment(p *Payment) error { return err } - const stmt = `DELETE FROM payments WHERE uuid=$1;` - _, err = tx.Exec(stmt, p.UUID) + _, err = tx.Exec(deletePayment, p.UUID) if err != nil { tx.Rollback() return err @@ -569,11 +421,7 @@ func (db *PostgresDB) ArchivePayment(p *Payment) error { aPmt := NewPayment(p.Account, p.Source, p.Amount, p.Height, p.EstimatedMaturity) - const stmt2 = `INSERT INTO archivedpayments( - uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);` - _, err = tx.Exec(stmt2, + _, err = tx.Exec(insertArchivedPayment, aPmt.UUID, aPmt.Account, aPmt.EstimatedMaturity, aPmt.Height, aPmt.Amount, aPmt.CreatedOn, aPmt.PaidOnHeight, aPmt.TransactionID, aPmt.Source.BlockHash, p.Source.Coinbase) @@ -588,13 +436,7 @@ func (db *PostgresDB) ArchivePayment(p *Payment) error { // fetchPaymentsAtHeight returns all payments sourcing from orphaned blocks at // the provided height. func (db *PostgresDB) fetchPaymentsAtHeight(height uint32) ([]*Payment, error) { - const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - FROM payments - WHERE paidonheight=0 - AND $1>(estimatedmaturity+1);` - - rows, err := db.DB.Query(stmt, height) + rows, err := db.DB.Query(selectPaymentsAtHeight, height) if err != nil { return nil, err } @@ -604,12 +446,7 @@ func (db *PostgresDB) fetchPaymentsAtHeight(height uint32) ([]*Payment, error) { // fetchPendingPayments fetches all unpaid payments. func (db *PostgresDB) fetchPendingPayments() ([]*Payment, error) { - const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - FROM payments - WHERE paidonheight=0;` - - rows, err := db.DB.Query(stmt) + rows, err := db.DB.Query(selectPendingPayments) if err != nil { return nil, err } @@ -620,13 +457,8 @@ func (db *PostgresDB) fetchPendingPayments() ([]*Payment, error) { // pendingPaymentsForBlockHash returns the number of pending payments with the // provided block hash as their source. func (db *PostgresDB) pendingPaymentsForBlockHash(blockHash string) (uint32, error) { - const stmt = `SELECT count(1) - FROM payments - WHERE paidonheight=0 - AND sourceblockhash=$1;` - var count uint32 - err := db.DB.QueryRow(stmt, blockHash).Scan(&count) + err := db.DB.QueryRow(countPaymentsAtBlockHash, blockHash).Scan(&count) if err != nil { return 0, err } @@ -637,12 +469,7 @@ func (db *PostgresDB) pendingPaymentsForBlockHash(blockHash string) (uint32, err // archivedPayments fetches all archived payments. List is ordered, most // recent comes first. func (db *PostgresDB) archivedPayments() ([]*Payment, error) { - const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - FROM archivedpayments - ORDER BY height DESC;` - - rows, err := db.DB.Query(stmt) + rows, err := db.DB.Query(selectArchivedPayments) if err != nil { return nil, err } @@ -653,13 +480,7 @@ func (db *PostgresDB) archivedPayments() ([]*Payment, error) { // maturePendingPayments fetches all mature pending payments at the // provided height. func (db *PostgresDB) maturePendingPayments(height uint32) (map[string][]*Payment, error) { - const stmt = `SELECT uuid, account, estimatedmaturity, height, amount, createdon, - paidonheight, transactionid, sourceblockhash, sourcecoinbase - FROM payments - WHERE paidonheight=0 - AND (estimatedmaturity+1)<=$1;` - - rows, err := db.DB.Query(stmt, height) + rows, err := db.DB.Query(selectMaturePendingPayments, height) if err != nil { return nil, err } @@ -686,10 +507,9 @@ func (db *PostgresDB) maturePendingPayments(height uint32) (map[string][]*Paymen // if the share is not found. func (db *PostgresDB) fetchShare(id string) (*Share, error) { const funcName = "fetchShare" - const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE uuid=$1;` var uuid, account, weight string var createdOn int64 - err := db.DB.QueryRow(stmt, id).Scan(&uuid, &account, &weight, &createdOn) + err := db.DB.QueryRow(selectShare, id).Scan(&uuid, &account, &weight, &createdOn) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no share found for id %s", funcName, id) @@ -713,8 +533,8 @@ func (db *PostgresDB) fetchShare(id string) (*Share, error) { // already exists with the same ID. func (db *PostgresDB) PersistShare(share *Share) error { const funcName = "PersistShare" - const stmt = `INSERT INTO shares(uuid, account, weight, createdon) VALUES ($1,$2,$3,$4);` - _, err := db.DB.Exec(stmt, share.UUID, share.Account, share.Weight.RatString(), share.CreatedOn) + + _, err := db.DB.Exec(insertShare, share.UUID, share.Account, share.Weight.RatString(), share.CreatedOn) if err != nil { var pqError *pq.Error @@ -733,9 +553,7 @@ func (db *PostgresDB) PersistShare(share *Share) error { // ppsEligibleShares fetches all shares created before or at the provided time. func (db *PostgresDB) ppsEligibleShares(max int64) ([]*Share, error) { - const funcName = "ppsEligibleShares" - const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE createdon <= $1` - rows, err := db.DB.Query(stmt, max) + rows, err := db.DB.Query(selectSharesOnOrBeforeTime, max) if err != nil { return nil, err } @@ -745,9 +563,7 @@ func (db *PostgresDB) ppsEligibleShares(max int64) ([]*Share, error) { // pplnsEligibleShares fetches all shares created after the provided time. func (db *PostgresDB) pplnsEligibleShares(min int64) ([]*Share, error) { - const funcName = "pplnsEligibleShares" - const stmt = `SELECT uuid, account, weight, createdon FROM shares WHERE createdon > $1` - rows, err := db.DB.Query(stmt, min) + rows, err := db.DB.Query(selectSharesAfterTime, min) if err != nil { return nil, err } @@ -758,8 +574,7 @@ func (db *PostgresDB) pplnsEligibleShares(min int64) ([]*Share, error) { // pruneShares removes shares with a createdOn time earlier than the provided // time. func (db *PostgresDB) pruneShares(minNano int64) error { - const stmt = `DELETE FROM shares WHERE createdon < $1` - _, err := db.DB.Exec(stmt, minNano) + _, err := db.DB.Exec(deleteShareCreatedBefore, minNano) return err } @@ -767,15 +582,12 @@ func (db *PostgresDB) pruneShares(minNano int64) error { // Returns an error if the work is not found. func (db *PostgresDB) fetchAcceptedWork(id string) (*AcceptedWork, error) { const funcName = "fetchAcceptedWork" - const stmt = `SELECT - uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed - FROM acceptedwork WHERE uuid=$1;` var uuid, blockhash, prevhash, minedby, miner string var confirmed bool var height uint32 var createdOn int64 - err := db.DB.QueryRow(stmt, id).Scan(&uuid, &blockhash, &prevhash, &height, + err := db.DB.QueryRow(selectAcceptedWork, id).Scan(&uuid, &blockhash, &prevhash, &height, &minedby, &miner, &createdOn, &confirmed) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -794,10 +606,7 @@ func (db *PostgresDB) fetchAcceptedWork(id string) (*AcceptedWork, error) { func (db *PostgresDB) persistAcceptedWork(work *AcceptedWork) error { const funcName = "persistAcceptedWork" - const stmt = `INSERT INTO acceptedwork( - uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8);` - _, err := db.DB.Exec(stmt, work.UUID, work.BlockHash, work.PrevHash, + _, err := db.DB.Exec(insertAcceptedWork, work.UUID, work.BlockHash, work.PrevHash, work.Height, work.MinedBy, work.Miner, work.CreatedOn, work.Confirmed) if err != nil { @@ -819,16 +628,8 @@ func (db *PostgresDB) persistAcceptedWork(work *AcceptedWork) error { // error if the work is not found. func (db *PostgresDB) updateAcceptedWork(work *AcceptedWork) error { const funcName = "updateAcceptedWork" - const stmt = `UPDATE acceptedwork SET - blockhash=$2, - prevhash=$3, - height=$4, - minedby=$5, - miner=$6, - createdon=$7, - confirmed=$8 - WHERE uuid=$1;` - result, err := db.DB.Exec(stmt, + + result, err := db.DB.Exec(updateAcceptedWork, work.UUID, work.BlockHash, work.PrevHash, work.Height, work.MinedBy, work.Miner, work.CreatedOn, work.Confirmed) if err != nil { @@ -850,8 +651,7 @@ func (db *PostgresDB) updateAcceptedWork(work *AcceptedWork) error { // deleteAcceptedWork removes the associated accepted work from the database. func (db *PostgresDB) deleteAcceptedWork(id string) error { - const stmt = `DELETE FROM acceptedwork WHERE uuid=$1;` - _, err := db.DB.Exec(stmt, id) + _, err := db.DB.Exec(deleteAcceptedWork, id) return err } @@ -860,30 +660,18 @@ func (db *PostgresDB) deleteAcceptedWork(id string) error { // // List is ordered, most recent comes first. func (db *PostgresDB) listMinedWork() ([]*AcceptedWork, error) { - const stmt = `SELECT - uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed - FROM acceptedwork - ORDER BY height DESC;` - - rows, err := db.DB.Query(stmt) + rows, err := db.DB.Query(selectMinedWork) if err != nil { return nil, err } return decodeWorkRows(rows) - } // fetchUnconfirmedWork returns all work which is not confirmed as mined with // height less than the provided height. func (db *PostgresDB) fetchUnconfirmedWork(height uint32) ([]*AcceptedWork, error) { - const stmt = `SELECT - uuid, blockhash, prevhash, height, minedby, miner, createdon, confirmed - FROM acceptedwork - WHERE $1>height - AND confirmed=false;` - - rows, err := db.DB.Query(stmt, height) + rows, err := db.DB.Query(selectUnconfirmedWork, height) if err != nil { return nil, err } @@ -895,10 +683,9 @@ func (db *PostgresDB) fetchUnconfirmedWork(height uint32) ([]*AcceptedWork, erro // the job is not found. func (db *PostgresDB) fetchJob(id string) (*Job, error) { const funcName = "fetchJob" - const stmt = `SELECT uuid, header, height FROM jobs WHERE uuid=$1;` var uuid, header string var height uint32 - err := db.DB.QueryRow(stmt, id).Scan(&uuid, &header, &height) + err := db.DB.QueryRow(selectJob, id).Scan(&uuid, &header, &height) if err != nil { if errors.Is(err, sql.ErrNoRows) { desc := fmt.Sprintf("%s: no job found for id %s", funcName, id) @@ -914,8 +701,8 @@ func (db *PostgresDB) fetchJob(id string) (*Job, error) { // already exists with the same ID. func (db *PostgresDB) persistJob(job *Job) error { const funcName = "persistJob" - const stmt = `INSERT INTO jobs(uuid, height, header) VALUES ($1,$2,$3);` - _, err := db.DB.Exec(stmt, job.UUID, job.Height, job.Header) + + _, err := db.DB.Exec(insertJob, job.UUID, job.Height, job.Header) if err != nil { var pqError *pq.Error @@ -934,15 +721,13 @@ func (db *PostgresDB) persistJob(job *Job) error { // deleteJob removes the associated job from the database. func (db *PostgresDB) deleteJob(id string) error { - const stmt = `DELETE FROM jobs WHERE uuid=$1;` - _, err := db.DB.Exec(stmt, id) + _, err := db.DB.Exec(deleteJob, id) return err } // deleteJobsBeforeHeight removes all jobs with heights less than the provided // height. func (db *PostgresDB) deleteJobsBeforeHeight(height uint32) error { - const stmt = `DELETE FROM jobs WHERE height < $1;` - _, err := db.DB.Exec(stmt, height) + _, err := db.DB.Exec(deleteJobBeforeHeight, height) return err } diff --git a/pool/sql_queries.go b/pool/sql_queries.go new file mode 100644 index 00000000..41c47266 --- /dev/null +++ b/pool/sql_queries.go @@ -0,0 +1,372 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package pool + +const ( + createTableMetadata = ` + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + );` + + createTableAccounts = ` + CREATE TABLE IF NOT EXISTS accounts ( + uuid TEXT PRIMARY KEY, + address TEXT NOT NULL, + createdon INT8 NOT NULL + );` + + createTablePayments = ` + CREATE TABLE IF NOT EXISTS payments ( + uuid TEXT PRIMARY KEY, + account TEXT NOT NULL, + estimatedmaturity INT8 NOT NULL, + height INT8 NOT NULL, + amount INT8 NOT NULL, + createdon INT8 NOT NULL, + paidonheight INT8 NOT NULL, + transactionid TEXT NOT NULL, + sourceblockhash TEXT NOT NULL, + sourcecoinbase TEXT NOT NULL + );` + + createTableArchivedPayments = ` + CREATE TABLE IF NOT EXISTS archivedpayments ( + uuid TEXT PRIMARY KEY, + account TEXT NOT NULL, + estimatedmaturity INT8 NOT NULL, + height INT8 NOT NULL, + amount INT8 NOT NULL, + createdon INT8 NOT NULL, + paidonheight INT8 NOT NULL, + transactionid TEXT NOT NULL, + sourceblockhash TEXT NOT NULL, + sourcecoinbase TEXT NOT NULL + );` + + createTableJobs = ` + CREATE TABLE IF NOT EXISTS jobs ( + uuid TEXT PRIMARY KEY, + height INT8 NOT NULL, + header TEXT NOT NULL + );` + + createTableShares = ` + CREATE TABLE IF NOT EXISTS shares ( + uuid TEXT PRIMARY KEY, + account TEXT NOT NULL, + weight TEXT NOT NULL, + createdon INT8 NOT NULL + );` + + createTableAcceptedWork = ` + CREATE TABLE IF NOT EXISTS acceptedwork ( + uuid TEXT PRIMARY KEY, + blockhash TEXT NOT NULL, + prevhash TEXT NOT NULL, + height INT8 NOT NULL, + minedby TEXT NOT NULL, + miner TEXT NOT NULL, + createdon INT8 NOT NULL, + confirmed BOOLEAN NOT NULL + );` + + selectPoolMode = ` + SELECT value + FROM metadata + WHERE key='poolmode';` + + insertPoolMode = ` + INSERT INTO metadata(key, value) + VALUES ('poolmode', $1) + ON CONFLICT (key) + DO UPDATE SET value=$1;` + + selectCSRFSecret = ` + SELECT value + FROM metadata + WHERE key='csrfsecret';` + + insertCSRFSecret = ` + INSERT INTO metadata(key, value) + VALUES ('csrfsecret', $1) + ON CONFLICT (key) + DO UPDATE SET value=$1;` + + selectLastPaymentHeight = ` + SELECT value + FROM metadata + WHERE key='lastpaymentheight';` + + insertLastPaymentHeight = ` + INSERT INTO metadata(key, value) + VALUES ('lastpaymentheight', $1) + ON CONFLICT (key) + DO UPDATE SET value=$1;` + + selectLastPaymentPaidOn = ` + SELECT value + FROM metadata + WHERE key='lastpaymentpaidon';` + + insertLastPaymentPaidOn = ` + INSERT INTO metadata(key, value) + VALUES ('lastpaymentpaidon', $1) + ON CONFLICT (key) + DO UPDATE SET value=$1;` + + selectLastPaymentCreatedOn = ` + SELECT value + FROM metadata + WHERE key='lastpaymentcreatedon';` + + insertLastPaymentCreatedOn = ` + INSERT INTO metadata(key, value) + VALUES ('lastpaymentcreatedon', $1) + ON CONFLICT (key) + DO UPDATE SET value=$1;` + + insertAccount = ` + INSERT INTO accounts( + uuid, address, createdon + ) VALUES ($1,$2,$3);` + + selectAccount = ` + SELECT + uuid, address, createdon + FROM accounts + WHERE uuid=$1;` + + deleteAccount = `DELETE FROM accounts WHERE uuid=$1;` + + insertPayment = ` + INSERT INTO payments( + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + ) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);` + + selectPayment = ` + SELECT + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + FROM payments + WHERE uuid=$1;` + + deletePayment = `DELETE FROM payments WHERE uuid=$1;` + + updatePayment = ` + UPDATE payments + SET + account=$2, + estimatedmaturity=$3, + height=$4, + amount=$5, + createdon=$6, + paidonheight=$7, + transactionid=$8, + sourceblockhash=$9, + sourcecoinbase=$10 + WHERE uuid=$1;` + + insertArchivedPayment = ` + INSERT INTO archivedpayments( + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + ) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);` + + selectPaymentsAtHeight = ` + SELECT + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + FROM payments + WHERE paidonheight=0 + AND $1>(estimatedmaturity+1);` + + selectPendingPayments = ` + SELECT + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + FROM payments + WHERE paidonheight=0;` + + countPaymentsAtBlockHash = ` + SELECT count(1) + FROM payments + WHERE paidonheight=0 + AND sourceblockhash=$1;` + + selectArchivedPayments = ` + SELECT + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + FROM archivedpayments + ORDER BY height DESC;` + + selectMaturePendingPayments = ` + SELECT + uuid, + account, + estimatedmaturity, + height, + amount, + createdon, + paidonheight, + transactionid, + sourceblockhash, + sourcecoinbase + FROM payments + WHERE paidonheight=0 + AND (estimatedmaturity+1)<=$1;` + + selectShare = ` + SELECT + uuid, account, weight, createdon + FROM shares + WHERE uuid=$1;` + + insertShare = ` + INSERT INTO shares( + uuid, account, weight, createdon + ) + VALUES ($1,$2,$3,$4);` + + selectSharesOnOrBeforeTime = ` + SELECT + uuid, account, weight, createdon + FROM shares + WHERE createdon <= $1` + + selectSharesAfterTime = ` + SELECT + uuid, account, weight, createdon + FROM shares + WHERE createdon > $1` + + deleteShareCreatedBefore = `DELETE FROM shares WHERE createdon < $1` + + selectAcceptedWork = ` + SELECT + uuid, + blockhash, + prevhash, + height, + minedby, + miner, + createdon, + confirmed + FROM acceptedwork + WHERE uuid=$1;` + + insertAcceptedWork = ` + INSERT INTO acceptedwork( + uuid, + blockhash, + prevhash, + height, + minedby, + miner, + createdon, + confirmed + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8);` + + updateAcceptedWork = ` + UPDATE acceptedwork + SET + blockhash=$2, + prevhash=$3, + height=$4, + minedby=$5, + miner=$6, + createdon=$7, + confirmed=$8 + WHERE uuid=$1;` + + deleteAcceptedWork = `DELETE FROM acceptedwork WHERE uuid=$1;` + + selectMinedWork = ` + SELECT + uuid, + blockhash, + prevhash, + height, + minedby, + miner, + createdon, + confirmed + FROM acceptedwork + ORDER BY height DESC;` + + selectUnconfirmedWork = ` + SELECT + uuid, + blockhash, + prevhash, + height, + minedby, + miner, + createdon, + confirmed + FROM acceptedwork + WHERE $1>height + AND confirmed=false;` + + selectJob = `SELECT uuid, header, height FROM jobs WHERE uuid=$1;` + + insertJob = `INSERT INTO jobs(uuid, height, header) VALUES ($1,$2,$3);` + + deleteJob = `DELETE FROM jobs WHERE uuid=$1;` + + deleteJobBeforeHeight = `DELETE FROM jobs WHERE height < $1;` +) From 1ab84ae4dc1f7a8e3d366f495f5b57ce24b5daeb Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 5 Nov 2020 10:40:42 +0000 Subject: [PATCH 5/5] Additional commentary --- pool/boltdb.go | 24 +++++++++++++++++------- pool/db_test.go | 18 +++++++++--------- pool/postgres.go | 21 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/pool/boltdb.go b/pool/boltdb.go index a4d2a2f6..84d46bcf 100644 --- a/pool/boltdb.go +++ b/pool/boltdb.go @@ -228,11 +228,9 @@ func nanoToBigEndianBytes(nano int64) []byte { return b } +// fetchPoolMode retrives the pool mode from the database. PoolMode is stored as +// a uint32 for historical reasons. 0 indicates Public, 1 indicates Solo. func (db *BoltDB) fetchPoolMode() (uint32, error) { - // PoolMode is stored as a uint32 for historical reasons. - // 0 indicates Public - // 1 indicates Solo - var mode uint32 err := db.DB.View(func(tx *bolt.Tx) error { pbkt, err := fetchPoolBucket(tx) @@ -253,10 +251,10 @@ func (db *BoltDB) fetchPoolMode() (uint32, error) { return mode, nil } +// persistPoolMode stores the pool mode in the database. PoolMode is stored as a +// uint32 for historical reasons. 0 indicates Public, 1 indicates Solo. func (db *BoltDB) persistPoolMode(mode uint32) error { - // PoolMode is stored as a uint32 for historical reasons. - // 0 indicates Public - // 1 indicates Solo + return db.DB.Update(func(tx *bolt.Tx) error { pbkt := tx.Bucket(poolBkt) b := make([]byte, 4) @@ -265,6 +263,7 @@ func (db *BoltDB) persistPoolMode(mode uint32) error { }) } +// fetchCSRFSecret retrieves the bytes used for the CSRF secret from the database. func (db *BoltDB) fetchCSRFSecret() ([]byte, error) { var secret []byte @@ -293,6 +292,7 @@ func (db *BoltDB) fetchCSRFSecret() ([]byte, error) { return secret, nil } +// persistCSRFSecret stores the bytes used for the CSRF secret in the database. func (db *BoltDB) persistCSRFSecret(secret []byte) error { return db.DB.Update(func(tx *bolt.Tx) error { pbkt := tx.Bucket(poolBkt) @@ -305,6 +305,8 @@ func (db *BoltDB) persistCSRFSecret(secret []byte) error { }) } +// persistLastPaymentInfo stores the last payment height and paidOn timestamp +// in the database. func (db *BoltDB) persistLastPaymentInfo(height uint32, paidOn int64) error { funcName := "persistLastPaymentInfo" return db.DB.Update(func(tx *bolt.Tx) error { @@ -333,6 +335,8 @@ func (db *BoltDB) persistLastPaymentInfo(height uint32, paidOn int64) error { }) } +// loadLastPaymentInfo retrieves the last payment height and paidOn timestamp +// from the database. func (db *BoltDB) loadLastPaymentInfo() (uint32, int64, error) { funcName := "loadLastPaymentInfo" var height uint32 @@ -364,6 +368,8 @@ func (db *BoltDB) loadLastPaymentInfo() (uint32, int64, error) { return height, paidOn, nil } +// persistLastPaymentCreatedOn stores the last payment createdOn timestamp in +// the database. func (db *BoltDB) persistLastPaymentCreatedOn(createdOn int64) error { funcName := "persistLastPaymentCreatedOn" return db.DB.Update(func(tx *bolt.Tx) error { @@ -381,6 +387,8 @@ func (db *BoltDB) persistLastPaymentCreatedOn(createdOn int64) error { }) } +// loadLastPaymentCreatedOn retrieves the last payment createdOn timestamp from +// the database. func (db *BoltDB) loadLastPaymentCreatedOn() (int64, error) { funcName := "loadLastPaymentCreatedOn" var createdOn int64 @@ -411,6 +419,8 @@ func (db *BoltDB) Close() error { return db.DB.Close() } +// httpBackup streams a backup of the entire database over the provided HTTP +// response writer. func (db *BoltDB) httpBackup(w http.ResponseWriter) error { err := db.DB.View(func(tx *bolt.Tx) error { w.Header().Set("Content-Type", "application/octet-stream") diff --git a/pool/db_test.go b/pool/db_test.go index 0a3db7b8..ff9e76da 100644 --- a/pool/db_test.go +++ b/pool/db_test.go @@ -231,23 +231,23 @@ func testLastPaymentCreatedOn(t *testing.T) { // Expect an error if no value set. _, err := db.loadLastPaymentCreatedOn() if !errors.Is(err, ErrValueNotFound) { - t.Fatalf("[loadLastPaymentCreatedOn] expected value not found error, got: %v", err) + t.Fatalf("expected value not found error, got: %v", err) } // Set some values. lastPaymentCreatedOn := time.Now().UnixNano() err = db.persistLastPaymentCreatedOn(lastPaymentCreatedOn) if err != nil { - t.Fatalf("[persistLastPaymentCreatedOn] unable to persist last payment created on: %v", err) + t.Fatalf("unable to persist last payment created on: %v", err) } // Ensure values can be retrieved. paymentCreatedOn, err := db.loadLastPaymentCreatedOn() if err != nil { - t.Fatalf("[loadLastPaymentCreatedOn] unable to load last payment created on: %v", err) + t.Fatalf("unable to load last payment created on: %v", err) } if lastPaymentCreatedOn != paymentCreatedOn { - t.Fatalf("[loadLastPaymentCreatedOn] expected last payment created on to be %d, got %d", + t.Fatalf("expected last payment created on to be %d, got %d", lastPaymentCreatedOn, paymentCreatedOn) } } @@ -294,7 +294,7 @@ func testLastPaymentInfo(t *testing.T) { // Expect an error if no value set. _, _, err := db.loadLastPaymentInfo() if !errors.Is(err, ErrValueNotFound) { - t.Fatalf("[loadLastPaymentInfo] expected value not found error, got: %v", err) + t.Fatalf("expected value not found error, got: %v", err) } // Set some values. @@ -302,22 +302,22 @@ func testLastPaymentInfo(t *testing.T) { lastPaymentPaidOn := time.Now().UnixNano() err = db.persistLastPaymentInfo(lastPaymentHeight, lastPaymentPaidOn) if err != nil { - t.Fatalf("[persistLastPaymentInfo] unable to persist last payment info: %v", err) + t.Fatalf("unable to persist last payment info: %v", err) } // Ensure values can be retrieved. paymentHeight, paymentPaidOn, err := db.loadLastPaymentInfo() if err != nil { - t.Fatalf("[loadLastPaymentInfo] unable to load last payment info: %v", err) + t.Fatalf("unable to load last payment info: %v", err) } if lastPaymentHeight != paymentHeight { - t.Fatalf("[loadLastPaymentInfo] expected last payment height to be %d, got %d", + t.Fatalf("expected last payment height to be %d, got %d", paymentHeight, paymentHeight) } if lastPaymentPaidOn != paymentPaidOn { - t.Fatalf("[loadLastPaymentInfo] expected last payment paid on to be %d, got %d", + t.Fatalf("expected last payment paid on to be %d, got %d", lastPaymentPaidOn, paymentPaidOn) } } diff --git a/pool/postgres.go b/pool/postgres.go index 8092ddb6..e5e276f0 100644 --- a/pool/postgres.go +++ b/pool/postgres.go @@ -42,6 +42,7 @@ func InitPostgresDB(host string, port uint32, user, pass, dbName string) (*Postg return nil, dbError(ErrDBOpen, desc) } + // Create all of the tables required by dcrpool. _, err = db.Exec(createTableMetadata) if err != nil { return nil, err @@ -85,6 +86,8 @@ func (db *PostgresDB) Close() error { return db.DB.Close() } +// decodePaymentRows deserializes the provided SQL rows into a slice of Payment +// structs. func decodePaymentRows(rows *sql.Rows) ([]*Payment, error) { var toReturn []*Payment for rows.Next() { @@ -112,6 +115,8 @@ func decodePaymentRows(rows *sql.Rows) ([]*Payment, error) { return toReturn, nil } +// decodeWorkRows deserializes the provided SQL rows into a slice of +// AcceptedWork structs. func decodeWorkRows(rows *sql.Rows) ([]*AcceptedWork, error) { var toReturn []*AcceptedWork for rows.Next() { @@ -138,6 +143,8 @@ func decodeWorkRows(rows *sql.Rows) ([]*AcceptedWork, error) { return toReturn, nil } +// decodeShareRows deserializes the provided SQL rows into a slice of Share +// structs. func decodeShareRows(rows *sql.Rows) ([]*Share, error) { var toReturn []*Share for rows.Next() { @@ -175,6 +182,8 @@ func (db *PostgresDB) Backup(fileName string) error { return errors.New("Backup is not implemented for postgres database") } +// fetchPoolMode retrives the pool mode from the database. PoolMode is stored as +// a uint32 for historical reasons. 0 indicates Public, 1 indicates Solo. func (db *PostgresDB) fetchPoolMode() (uint32, error) { const funcName = "fetchPoolMode" var poolmode uint32 @@ -190,11 +199,14 @@ func (db *PostgresDB) fetchPoolMode() (uint32, error) { return poolmode, nil } +// persistPoolMode stores the pool mode in the database. PoolMode is stored as a +// uint32 for historical reasons. 0 indicates Public, 1 indicates Solo. func (db *PostgresDB) persistPoolMode(mode uint32) error { _, err := db.DB.Exec(insertPoolMode, mode) return err } +// fetchCSRFSecret retrieves the bytes used for the CSRF secret from the database. func (db *PostgresDB) fetchCSRFSecret() ([]byte, error) { const funcName = "fetchCSRFSecret" var secret string @@ -218,11 +230,14 @@ func (db *PostgresDB) fetchCSRFSecret() ([]byte, error) { return decoded, nil } +// persistCSRFSecret stores the bytes used for the CSRF secret in the database. func (db *PostgresDB) persistCSRFSecret(secret []byte) error { _, err := db.DB.Exec(insertCSRFSecret, hex.EncodeToString(secret)) return err } +// persistLastPaymentInfo stores the last payment height and paidOn timestamp +// in the database. func (db *PostgresDB) persistLastPaymentInfo(height uint32, paidOn int64) error { tx, err := db.DB.Begin() if err != nil { @@ -244,6 +259,8 @@ func (db *PostgresDB) persistLastPaymentInfo(height uint32, paidOn int64) error return tx.Commit() } +// loadLastPaymentInfo retrieves the last payment height and paidOn timestamp +// from the database. func (db *PostgresDB) loadLastPaymentInfo() (uint32, int64, error) { const funcName = "loadLastPaymentInfo" @@ -274,11 +291,15 @@ func (db *PostgresDB) loadLastPaymentInfo() (uint32, int64, error) { return height, paidOn, nil } +// persistLastPaymentCreatedOn stores the last payment createdOn timestamp in +// the database. func (db *PostgresDB) persistLastPaymentCreatedOn(createdOn int64) error { _, err := db.DB.Exec(insertLastPaymentCreatedOn, createdOn) return err } +// loadLastPaymentCreatedOn retrieves the last payment createdOn timestamp from +// the database. func (db *PostgresDB) loadLastPaymentCreatedOn() (int64, error) { const funcName = "loadLastPaymentCreatedOn" var createdOn int64