Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package main
- // GORM appears to be swallowing the pq error codes returned by crdb_internal.force_retry() altogether. The output at the end of this paste shows the error message from pq, but it's not exposed via GORM's db.Error, so the code below can't see it, and the retry loop thinks everything is ok when it definitely is not.
- import (
- "fmt"
- "log"
- "math"
- "math/rand"
- "time"
- // Import GORM-related packages.
- "github.com/jinzhu/gorm"
- _ "github.com/jinzhu/gorm/dialects/postgres"
- )
- // 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()
- 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})
- printBalances(db)
- // Transfer funds between accounts. To handle any possible
- // transaction retry errors, we add a retry loop with exponential
- // backoff to the transfer logic (see below).
- var maxRetries = 3
- var amount = 100
- var fromAccount Account
- var toAccount Account
- db.First(&fromAccount, 1)
- db.First(&toAccount, 2)
- retryLoop:
- for retries := 0; retries <= maxRetries; retries++ {
- if retries == maxRetries {
- fmt.Errorf("hit max of %d retries, aborting", retries)
- break retryLoop
- }
- err := transferFunds(db, fromAccount, toAccount, amount)
- fmt.Printf("transferFunds result struct: %+v\n", err)
- if err != nil {
- // If there's an error, we try again after sleeping an
- // increased amount of time, a.k.a. exponential backoff.
- r := rand.New(rand.NewSource(time.Now().UnixNano()))
- var sleepMs = ((int(math.Pow(2, float64(retries))) * 100) + r.Intn(100-1) + 1)
- time.Sleep(time.Millisecond * time.Duration(sleepMs))
- } else {
- fmt.Println("no errors, breaking from 'retryLoop'!")
- break retryLoop
- }
- }
- // 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 {
- err := fmt.Errorf("account %d balance %d is lower than transfer amount %d", fromAccount.ID, fromAccount.Balance, amount)
- return err
- }
- // We would like to check for the 40001 error code below, but it
- // isn't clear how to get at it from GORM. According to
- // https://github.com/jinzhu/gorm/issues/17, GORM does not expose
- // the PG error codes. Therefore, we retry on any error.
- // if err := db.Exec(
- // `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 {
- // db.Exec("ROLLBACK")
- // return db.Error
- // }
- db.Exec("BEGIN")
- db.Exec("SELECT now()") // Prevent automatic server-side retries.
- if err := db.Exec("SELECT crdb_internal.force_retry('1s':::INTERVAL)").Error; err != nil {
- fmt.Printf("db.Error: %+v\n", db.Error)
- db.Exec("ROLLBACK")
- } else {
- db.Exec("COMMIT")
- }
- 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
- } else {
- return nil
- }
- }
- // make
- // go run gorm-sample.go
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:37)
- // [2019-07-18 09:58:35] [36;1m[1.18ms] INSERT INTO "accounts" ("id","balance") VALUES ('1','1000') RETURNING "accounts"."id"
- // [1 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:38)
- // [2019-07-18 09:58:35] [36;1m[0.86ms] INSERT INTO "accounts" ("id","balance") VALUES ('2','250') RETURNING "accounts"."id"
- // [1 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:121)
- // [2019-07-18 09:58:35] [36;1m[1.97ms] SELECT * FROM "accounts"
- // [2 rows affected or returned ]
- // Balance at '2019-07-18 09:58:35.138652 -0400 EDT m=+0.025357396':
- // 1 1000
- // 2 250
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:52)
- // [2019-07-18 09:58:35] [36;1m[0.77ms] SELECT * FROM "accounts" WHERE ("accounts"."id" = 1) ORDER BY "accounts"."id" ASC LIMIT 1
- // [1 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:53)
- // [2019-07-18 09:58:35] [36;1m[0.58ms] SELECT * FROM "accounts" WHERE ("accounts"."id" = 2) ORDER BY "accounts"."id" ASC LIMIT 1
- // [1 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:108)
- // [2019-07-18 09:58:35] [36;1m[0.13ms] BEGIN
- // [0 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:109)
- // [2019-07-18 09:58:35] [36;1m[0.22ms] SELECT now()
- // [1 rows affected or returned ]
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:110)
- // [2019-07-18 09:58:35][31;1m pq: restart transaction: crdb_internal.force_retry(): TransactionRetryWithProtoRefreshError: forced by crdb_internal.force_retry() [0m
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:110)
- // [2019-07-18 09:58:35] [36;1m[52.03ms] SELECT crdb_internal.force_retry('1s':::INTERVAL)
- // [0 rows affected or returned ]
- // db.Error: <nil>
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:112)
- // [2019-07-18 09:58:35] [36;1m[0.28ms] ROLLBACK
- // [0 rows affected or returned ]
- // transferFunds result struct: <nil>
- // no errors, breaking from 'retryLoop'!
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:121)
- // [2019-07-18 09:58:35] [36;1m[0.70ms] SELECT * FROM "accounts"
- // [2 rows affected or returned ]
- // Balance at '2019-07-18 09:58:35.193928 -0400 EDT m=+0.080633354':
- // 1 1000
- // 2 250
- // (/Users/rloveland/work/code/deemphasize-savepoints/golang/gorm-sample.go:131)
- // [2019-07-18 09:58:35] [36;1m[1.90ms] DELETE from accounts where ID > 0
- // [2 rows affected or returned ]
- // Compilation finished at Thu Jul 18 09:58:35
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement