Skip to content

Commit

Permalink
ref!(db): upgrade retry and pgx versions
Browse files Browse the repository at this point in the history
In this commit the support for Go < 1.20 is dropped.
  • Loading branch information
arsham committed Feb 27, 2023
1 parent 91dd471 commit 8c55f74
Show file tree
Hide file tree
Showing 17 changed files with 371 additions and 497 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: ["1.18", "1.19"]
go: ["1.20"]

steps:
- name: Checkout repo
Expand Down Expand Up @@ -58,11 +58,16 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v3

- name: Set up Go
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}

- name: Check for go vulnerabilities
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: WriteGoList
run: go list -json -deps > go.list

Expand Down
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ run:

linters:
enable:
- asasalint
- bidichk
- bodyclose
- cyclop
Expand All @@ -119,20 +120,25 @@ linters:
- errcheck
- errname
- errorlint
- execinquery
- exportloopref
- forcetypeassert
- gocheckcompilerdirectives
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- godox
- gofmt
- gofumpt
- goprintffuncname
- gosec
- gosimple
- govet
- grouper
- ineffassign
- maintidx
- misspell
- nakedret
- nestif
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies: ## Install dependencies requried for development operations.
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@go install github.com/psampaz/go-mod-outdated@latest
@go install github.com/jondot/goweight@latest
@go install golang.org/x/vuln/cmd/govulncheck@latest
@go get -t -u golang.org/x/tools/cmd/cover
@go get -t -u github.com/sonatype-nexus-community/nancy@latest
@go get -u ./...
Expand All @@ -57,6 +58,7 @@ coverage: ## Show the test coverage on browser.

.PHONY: audit
audit: ## Audit the code for updates, vulnerabilities and binary weight.
govulncheck ./...
go list -u -m -json all | go-mod-outdated -update -direct
go list -json -deps | nancy sleuth
goweight | head -n 20
147 changes: 77 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ they succeed and handles errors in a developer friendly way. There are helpers
for using with [go-sqlmock][go-sqlmock] in tests. There is also a `Mocha`
inspired reporter for [spec BDD library][spec].

This library supports `Go >= 1.18`. To use this library use this import path:
This library supports `Go >= 1.20`. To use this library use this import path:

```go
```
github.com/arsham/dbtools/v3
```

For older Go's support use the v2:

```
github.com/arsham/dbtools/v2
```

Expand All @@ -36,22 +42,22 @@ taking care of errors. For example instead of writing:
```go
tx, err := db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "starting transaction")
return errors.Wrap(err, "starting transaction")
}
err := firstQueryCall(tx)
if err != nil {
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
}
err := secondQueryCall(tx)
if err != nil {
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
}
err := thirdQueryCall(tx)
if err != nil {
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
e := errors.Wrap(tx.Rollback(ctx), "rolling back transaction")
return multierror.Append(err, e).ErrorOrNil()
}

return errors.Wrap(tx.Commit(ctx), "committing transaction")
Expand Down Expand Up @@ -79,8 +85,8 @@ You can prematurely stop retrying by returning a `*retry.StopError` error:

```go
err = p.Transaction(ctx, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, query)
return &retry.StopError{Err: err}
_, err := tx.Exec(ctx, query)
return &retry.StopError{Err: err}
})
```

Expand All @@ -94,13 +100,13 @@ time until your queries succeed:
p, err := dbtools.NewPGX(conn, dbtools.Retry(20))
// handle the error
err = p.Transaction(ctx, func(tx pgx.Tx) error {
// use tx to run your queries
return someErr
}, func(tx pgx.Tx) error {
return someErr
}, func(tx pgx.Tx) error {
return someErr
// add more callbacks if required.
// use tx to run your queries
return someErr
}, func(tx pgx.Tx) error {
return someErr
}, func(tx pgx.Tx) error {
return someErr
// add more callbacks if required.
})
// handle the error!
```
Expand All @@ -111,12 +117,12 @@ Stop retrying when the row is not found:

```go
err := retrier.Do(func() error {
const query = `SELECT foo FROM bar WHERE id = $1::int`
err := conn.QueryRow(ctx, query, msgID).Scan(&foo)
if errors.Is(err, pgx.ErrNoRows) {
return &retry.StopError{Err: ErrFooNotFound}
}
return errors.Wrap(err, "quering database")
const query = `SELECT foo FROM bar WHERE id = $1::int`
err := conn.QueryRow(ctx, query, msgID).Scan(&foo)
if errors.Is(err, pgx.ErrNoRows) {
return &retry.StopError{Err: ErrFooNotFound}
}
return errors.Wrap(err, "quering database")
})
```

Expand Down Expand Up @@ -164,25 +170,26 @@ a common pattern for querying for multiple rows:
```go
result := make([]Result, 0, expectedTotal)
err := retrier.Do(func() error {
rows, err := r.pool.Query(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "making query")
}
defer rows.Close()

// make sure you reset the slice, otherwise in the next retry it adds the
// same data to the slice again.
result = result[:0]
for rows.Next() {
var doc Result
err := rows.Scan(&doc.A, &doc.B)
if err != nil {
return errors.Wrap(err, "scanning rows")
}
result = append(result, doc)
}

return errors.Wrap(rows.Err(), "row error")
rows, err := r.pool.Query(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "making query")
}

defer rows.Close()

// make sure you reset the slice, otherwise in the next retry it adds the
// same data to the slice again.
result = result[:0]
for rows.Next() {
var doc Result
err := rows.Scan(&doc.A, &doc.B)
if err != nil {
return errors.Wrap(err, "scanning rows")
}
result = append(result, doc)
}

return errors.Wrap(rows.Err(), "row error")
})
// handle the error!
```
Expand All @@ -206,38 +213,38 @@ use the same value on next queries:
import "database/sql"

func TestFoo(t *testing.T) {
// ...
// assume num has been generated randomly
num := 666
_, err := tx.ExecContext(ctx, "INSERT INTO life (value) VALUE ($1)", num)
// error check
_, err = tx.ExecContext(ctx, "INSERT INTO reality (value) VALUE ($1)", num)
// error check
_, err = tx.ExecContext(ctx, "INSERT INTO everywhere (value) VALUE ($1)", num)
// error check
// ...
// assume num has been generated randomly
num := 666
_, err := tx.ExecContext(ctx, "INSERT INTO life (value) VALUE ($1)", num)
// error check
_, err = tx.ExecContext(ctx, "INSERT INTO reality (value) VALUE ($1)", num)
// error check
_, err = tx.ExecContext(ctx, "INSERT INTO everywhere (value) VALUE ($1)", num)
// error check
}
```

Your tests can be checked easily like this:

```go
import (
"github.com/arsham/dbtools/v2/dbtesting"
"github.com/DATA-DOG/go-sqlmock"
"github.com/DATA-DOG/go-sqlmock"
"github.com/arsham/dbtools/v3/dbtesting"
)

func TestFoo(t *testing.T) {
// ...
rec := dbtesting.NewValueRecorder()
mock.ExpectExec("INSERT INTO life .+").
WithArgs(rec.Record("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO reality .+").
WithArgs(rec.For("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO everywhere .+").
WithArgs(rec.For("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
// ...
rec := dbtesting.NewValueRecorder()
mock.ExpectExec("INSERT INTO life .+").
WithArgs(rec.Record("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO reality .+").
WithArgs(rec.For("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO everywhere .+").
WithArgs(rec.For("truth")).
WillReturnResult(sqlmock.NewResult(1, 1))
}
```

Expand All @@ -262,7 +269,7 @@ relevant to the current test), you can use `OkValue`.

```go
import (
"github.com/arsham/dbtools/v2/dbtesting"
"github.com/arsham/dbtools/v3/dbtesting"
"github.com/DATA-DOG/go-sqlmock"
)

Expand All @@ -287,12 +294,12 @@ mock.ExpectExec("INSERT INTO life .+").
### Usage

```go
import "github.com/arsham/dbtools/v2/dbtesting"
import "github.com/arsham/dbtools/v3/dbtesting"

func TestFoo(t *testing.T) {
spec.Run(t, "Foo", func(t *testing.T, when spec.G, it spec.S) {
// ...
}, spec.Report(&dbtesting.Mocha{}))
spec.Run(t, "Foo", func(t *testing.T, when spec.G, it spec.S) {
// ...
}, spec.Report(&dbtesting.Mocha{}))
}

```
Expand Down
62 changes: 19 additions & 43 deletions contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ package dbtools
import (
"context"
"database/sql"
"errors"
"time"

"github.com/arsham/retry"
"github.com/jackc/pgx/v4"
"github.com/pkg/errors"
"github.com/arsham/retry/v2"
"github.com/jackc/pgx/v5"
)

var (
Expand Down Expand Up @@ -55,51 +55,27 @@ type Tx interface {
// A ConfigFunc function sets up a Transaction.
type ConfigFunc func(*PGX)

// Retry sets the retrier.
func Retry(r retry.Retry) ConfigFunc {
return func(t *PGX) {
t.loop = r
// WithRetry sets the retrier. The default retrier tries only once.
func WithRetry(r retry.Retry) ConfigFunc {
return func(p *PGX) {
p.loop = r
}
}

// RetryCount defines a transaction should be tried n times. If n is 0, it will
// be set as 1.
func RetryCount(n int) ConfigFunc {
return func(t *PGX) {
t.loop.Attempts = n
// Retry sets the retry strategy. If you want to pass a Retry object you can
// use the WithRetry function instead.
func Retry(attempts int, delay time.Duration) ConfigFunc {
return func(p *PGX) {
p.loop.Attempts = attempts
p.loop.Delay = delay
}
}

// RetryDelay is the amount of delay between each unsuccessful tries. Set
// DelayMethod for the method of delay duration.
func RetryDelay(d time.Duration) ConfigFunc {
return func(t *PGX) {
t.loop.Delay = d
// GracePeriod sets the context timeout when doing a rollback. This context
// needs to be different from the context user is giving as the user's context
// might be cancelled. The default value is 30s.
func GracePeriod(delay time.Duration) ConfigFunc {
return func(p *PGX) {
p.gracePeriod = delay
}
}

// DelayMethod decides how to delay between each tries. Default is
// retry.StandardDelay.
func DelayMethod(m retry.DelayMethod) ConfigFunc {
return func(t *PGX) {
t.loop.Method = m
}
}

// trError is used for managing situations that an error is reported from the
// transaction, and the rollback would result in an error.
type trError struct {
err error
rollback error
}

func (t *trError) Error() string {
return t.Unwrap().Error()
}

func (t *trError) Unwrap() error {
if t.rollback == nil {
return t.err
}
return errors.Wrapf(t.err, "%v (rollback error)", t.rollback)
}
Loading

0 comments on commit 8c55f74

Please sign in to comment.