Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package main
- // Summary: Using txn := db.Begin() followed by txn.Exec(foo),
- // txn.{Commit,Rollback} as shown below does not appear to work (see log output at
- // bottom). However passing everything through db.Exec(foo) does,
- // including sending db.Exec(`BEGIN`), db.Exec('ROLLBACK'), etc. (see
- // code in PR at https://github.com/cockroachdb/docs/pull/5049 for the
- // db.Exec version).
- import (
- "fmt"
- "log"
- "math"
- "math/rand"
- "time"
- // Import GORM-related packages.
- "github.com/jinzhu/gorm"
- _ "github.com/jinzhu/gorm/dialects/postgres"
- // Necessary in order to check for transaction retry error codes.
- // See implementation below in `transferFunds`.
- "github.com/lib/pq"
- )
- // Account is our model, which corresponds to the "accounts" database
- // table.
- type Account struct {
- ID int `gorm:"primary_key"`
- Balance int
- }
- func main() {
- // Connect to the "bank" database as the "maxroach" user.
- const addr = "postgresql://root@localhost:26257/bank?sslmode=disable"
- db, err := gorm.Open("postgres", addr)
- if err != nil {
- log.Fatal(err)
- }
- defer db.Close()
- // Set to `true` and GORM will print out all DB queries.
- db.LogMode(true)
- // Automatically create the "accounts" table based on the Account
- // model.
- db.AutoMigrate(&Account{})
- // Insert two rows into the "accounts" table.
- db.Create(&Account{ID: 1, Balance: 1000})
- db.Create(&Account{ID: 2, Balance: 250})
- // The sequence of steps in this section is:
- // 1. Print account balances.
- // 2. Set up some Accounts and transfer funds between them.
- // 3. Print account balances again to verify the transfer occurred.
- // Print balances before transfer.
- printBalances(db)
- var amount = 100
- var fromAccount Account
- var toAccount Account
- db.First(&fromAccount, 1)
- db.First(&toAccount, 2)
- // Transfer funds between accounts. To handle any possible
- // transaction retry errors, we add a retry loop with exponential
- // backoff to the transfer logic (see implementation below).
- if err := transferFunds(db, fromAccount, toAccount, amount); err != nil {
- // If the error is returned, it's either:
- // 1. Not a transaction retry error, i.e., some other kind of
- // database error that you should handle here.
- // 2. A transaction retry error that has occurred more than N
- // times (defined by the `maxRetries` variable inside
- // `transferFunds`), in which case you will need to figure out
- // why your database access is resulting in so much contention
- // (see 'Understanding and avoiding transaction contention':
- // https://www.cockroachlabs.com/docs/stable/performance-best-practices-overview.html#understanding-and-avoiding-transaction-contention)
- fmt.Println(err)
- }
- // Print balances after transfer to ensure that it worked.
- printBalances(db)
- // Delete accounts so we can start fresh when we want to run this
- // program again.
- deleteAccounts(db)
- }
- func transferFunds(db *gorm.DB, fromAccount Account, toAccount Account, amount int) error {
- if fromAccount.Balance < amount {
- return fmt.Errorf("account %d balance %d is lower than transfer amount %d", fromAccount.ID, fromAccount.Balance, amount)
- }
- var maxRetries = 3
- for retries := 0; retries <= maxRetries; retries++ {
- if retries == maxRetries {
- return fmt.Errorf("hit max of %d retries, aborting", retries)
- }
- // db.Exec("BEGIN")
- txn := db.Begin()
- txn.Exec(`SELECT now()`) // disable server-side auto-retries
- if err := txn.Exec(
- `SELECT crdb_internal.force_retry('1s'::INTERVAL)`,
- // `UPSERT INTO accounts (id, balance) VALUES
- // (?, ((SELECT balance FROM accounts WHERE id = ?) - ?)),
- // (?, ((SELECT balance FROM accounts WHERE id = ?) + ?))`,
- // fromAccount.ID, fromAccount.ID, amount,
- // toAccount.ID, toAccount.ID, amount
- ).Error; err != nil {
- // We need to cast GORM's db.Error to *pq.Error so we can
- // detect the Postgres transaction retry error code and
- // handle retries appropriately.
- pqErr := err.(*pq.Error)
- if pqErr.Code == "40001" {
- // Since this is a transaction retry error, we
- // ROLLBACK the transaction and sleep a little before
- // trying again. Each time through the loop we sleep
- // for a little longer than the last time
- // (A.K.A. exponential backoff).
- txn.Rollback()
- // db.Exec("ROLLBACK")
- var sleepMs = math.Pow(2, float64(retries)) * 100 * (rand.Float64() + 0.5)
- time.Sleep(time.Millisecond * time.Duration(sleepMs))
- } else {
- return err
- }
- } else {
- // Happy case. All went well, so we commit and break out
- // of the retry loop.
- txn.Commit()
- // db.Exec("COMMIT")
- break
- }
- }
- return nil
- }
- func printBalances(db *gorm.DB) {
- var accounts []Account
- db.Find(&accounts)
- fmt.Printf("Balance at '%s':\n", time.Now())
- for _, account := range accounts {
- fmt.Printf("%d %d\n", account.ID, account.Balance)
- }
- }
- func deleteAccounts(db *gorm.DB) error {
- // Used to tear down the accounts table so we can re-run this
- // program.
- err := db.Exec("DELETE from accounts where ID > 0").Error
- if err != nil {
- return err
- }
- return nil
- }
- // -*- mode: compilation; default-directory: "~/work/code/deemphasize-savepoints/golang/" -*-
- // Compilation started at Wed Jul 24 13:30:55
- // make -k
- // go run gorm-sample.go
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:43)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.77ms][0m INSERT INTO "accounts" ("id","balance") VALUES ('1','1000') RETURNING "accounts"."id"
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:44)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.69ms][0m INSERT INTO "accounts" ("id","balance") VALUES ('2','250') RETURNING "accounts"."id"
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:140)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.76ms][0m SELECT * FROM "accounts"
- // [36;31m[2 rows affected or returned ][0m
- // Balance at '2019-07-24 13:30:55.617543 -0400 EDT m=+0.019774217':
- // 1 1000
- // 2 250
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:58)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.48ms][0m SELECT * FROM "accounts" WHERE ("accounts"."id" = 1) ORDER BY "accounts"."id" ASC LIMIT 1
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:59)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.50ms][0m SELECT * FROM "accounts" WHERE ("accounts"."id" = 2) ORDER BY "accounts"."id" ASC LIMIT 1
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:98)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.61ms][0m SELECT now()
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:55][0m [31;1m pq: restart transaction: crdb_internal.force_retry(): TransactionRetryWithProtoRefreshError: forced by crdb_internal.force_retry() [0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.90ms][0m SELECT crdb_internal.force_retry('1s'::INTERVAL)
- // [36;31m[0 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:98)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[0.79ms][0m SELECT now()
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:55][0m [31;1m pq: restart transaction: crdb_internal.force_retry(): TransactionRetryWithProtoRefreshError: forced by crdb_internal.force_retry() [0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:55][0m [36;1m[1.07ms][0m SELECT crdb_internal.force_retry('1s'::INTERVAL)
- // [36;31m[0 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:98)[0m
- // [33m[2019-07-24 13:30:56][0m [36;1m[0.74ms][0m SELECT now()
- // [36;31m[1 rows affected or returned ][0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:56][0m [31;1m pq: restart transaction: crdb_internal.force_retry(): TransactionRetryWithProtoRefreshError: forced by crdb_internal.force_retry() [0m
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:99)[0m
- // [33m[2019-07-24 13:30:56][0m [36;1m[1.56ms][0m SELECT crdb_internal.force_retry('1s'::INTERVAL)
- // [36;31m[0 rows affected or returned ][0m
- // hit max of 3 retries, aborting
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:140)[0m
- // [33m[2019-07-24 13:30:56][0m [36;1m[1.44ms][0m SELECT * FROM "accounts"
- // [36;31m[2 rows affected or returned ][0m
- // Balance at '2019-07-24 13:30:56.533603 -0400 EDT m=+0.935833949':
- // 1 1000
- // 2 250
- // [35m(/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:150)[0m
- // [33m[2019-07-24 13:30:56][0m [36;1m[3.81ms][0m DELETE from accounts where ID > 0
- // [36;31m[2 rows affected or returned ][0m
- // Compilation finished at Wed Jul 24 13:30:56
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement