Skip to content

Commit b5ebaaa

Browse files
authored
crdb: Add ExecuteCtx for configurable retry limits (#180)
* crdb: Add ExecuteCtx for configurable retry limits Adds ExecuteCtx which allows configuring maximum retries through context, alongside the existing Execute function. This enables better control over retry behavior while maintaining compatibility with code using the original Execute. The new function helps prevent infinite retry loops in environments where bounded retry counts are required, while preserving the existing error handling and retry semantics.
1 parent eef1cc4 commit b5ebaaa

File tree

5 files changed

+176
-61
lines changed

5 files changed

+176
-61
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ jobs:
2727
strategy:
2828
matrix:
2929
go:
30-
- "1.19"
31-
- "1.20"
32-
- "1.21"
33-
- "1.22"
30+
- "1.23"
3431

3532
steps:
3633
- uses: actions/checkout@v2
@@ -42,11 +39,9 @@ jobs:
4239

4340
- name: Install dependencies
4441
run: |
45-
# The latest crlfmt requires go1.19.
46-
go install github.com/cockroachdb/[email protected]
47-
go install github.com/kisielk/[email protected]
42+
go install github.com/kisielk/errcheck@latest
4843
go install github.com/mdempsky/unconvert@latest
49-
go install honnef.co/go/tools/cmd/staticcheck@2023.1.7
44+
go install honnef.co/go/tools/cmd/staticcheck@latest
5045
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
5146
5247
- name: Build

crdb/tx.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,64 @@ func Execute(fn func() error) (err error) {
100100
}
101101
}
102102

103+
// ExecuteCtxFunc represents a function that takes a context and variadic
104+
// arguments and returns an error. It's used with ExecuteCtx to enable retryable
105+
// operations with configurable parameters.
106+
type ExecuteCtxFunc func(context.Context, ...interface{}) error
107+
108+
// ExecuteCtx runs fn and retries it as needed, respecting a maximum retry count
109+
// obtained from the context. It is used to add configurable retry handling to
110+
// the execution of a single statement. If a multi-statement transaction is
111+
// being run, use ExecuteTx instead.
112+
//
113+
// The maximum number of retries can be configured using WithMaxRetries(ctx, n).
114+
// Setting n=0 allows one attempt with no retries. If the number of retries is
115+
// exhausted and the last attempt resulted in a retryable error, ExecuteCtx
116+
// returns a max retries exceeded error wrapping the last retryable error
117+
// encountered.
118+
//
119+
// The fn parameter accepts variadic arguments which are passed through on each
120+
// retry attempt, allowing for flexible parameterization of the retried operation.
121+
//
122+
// As with Execute, retry handling for individual statements (implicit transactions)
123+
// is usually performed automatically on the CockroachDB SQL gateway, making use
124+
// of this function generally unnecessary. However, automatic retries are disabled
125+
// once result streaming begins (typically when results exceed 16KiB).
126+
//
127+
// NOTE: the supplied fn closure should not have external side effects beyond
128+
// changes to the database.
129+
//
130+
// fn must take care when wrapping errors returned from the database driver with
131+
// additional context. To preserve retry behavior, errors should implement either
132+
// `Cause() error` (github.com/pkg/errors) or `Unwrap() error` (Go 1.13+).
133+
// For example:
134+
//
135+
// crdb.ExecuteCtx(ctx, func(ctx context.Context, args ...interface{}) error {
136+
// id := args[0].(int)
137+
// rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
138+
// if err != nil {
139+
// return fmt.Errorf("scanning row: %w", err) // uses %w for proper error wrapping
140+
// }
141+
// defer rows.Close()
142+
// // ...
143+
// return nil
144+
// }, userID)
145+
func ExecuteCtx(ctx context.Context, fn ExecuteCtxFunc, args ...interface{}) (err error) {
146+
maxRetries := numRetriesFromContext(ctx)
147+
for n := 0; n <= maxRetries; n++ {
148+
if err = ctx.Err(); err != nil {
149+
return err
150+
}
151+
152+
err = fn(ctx, args...)
153+
if err == nil || !errIsRetryable(err) {
154+
return err
155+
}
156+
}
157+
158+
return newMaxRetriesExceededError(err, maxRetries)
159+
}
160+
103161
type txConfigKey struct{}
104162

105163
// WithMaxRetries configures context so that ExecuteTx retries tx specified

crdb/tx_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,75 @@ package crdb
1717
import (
1818
"context"
1919
"database/sql"
20+
"errors"
2021
"fmt"
2122
"testing"
2223

2324
"github.com/cockroachdb/cockroach-go/v2/testserver"
2425
)
2526

27+
// TestExecuteCtx verifies that ExecuteCtx correctly handles different retry limits
28+
// and context cancellation when executing database operations.
29+
//
30+
// TODO(seanc@): Add test cases that force retryable errors by simulating
31+
// transaction conflicts or network failures. Consider using the same write skew
32+
// pattern from TestExecuteTx.
33+
func TestExecuteCtx(t *testing.T) {
34+
db, stop := testserver.NewDBForTest(t)
35+
defer stop()
36+
ctx := context.Background()
37+
38+
// Setup test table
39+
if _, err := db.ExecContext(ctx, `CREATE TABLE test_retry (id INT PRIMARY KEY)`); err != nil {
40+
t.Fatal(err)
41+
}
42+
43+
testCases := []struct {
44+
name string
45+
maxRetries int
46+
id int
47+
withCancel bool
48+
wantErr error
49+
}{
50+
{"no retries", 0, 0, false, nil},
51+
{"single retry", 1, 1, false, nil},
52+
{"cancelled context", 1, 2, true, context.Canceled},
53+
{"no args", 1, 3, false, nil},
54+
}
55+
56+
fn := func(ctx context.Context, args ...interface{}) error {
57+
if len(args) == 0 {
58+
_, err := db.ExecContext(ctx, `INSERT INTO test_retry VALUES (3)`)
59+
return err
60+
}
61+
id := args[0].(int)
62+
_, err := db.ExecContext(ctx, `INSERT INTO test_retry VALUES ($1)`, id)
63+
return err
64+
}
65+
66+
for _, tc := range testCases {
67+
t.Run(tc.name, func(t *testing.T) {
68+
limitedCtx := WithMaxRetries(ctx, tc.maxRetries)
69+
if tc.withCancel {
70+
var cancel context.CancelFunc
71+
limitedCtx, cancel = context.WithCancel(limitedCtx)
72+
cancel()
73+
}
74+
75+
var err error
76+
if tc.name == "no args" {
77+
err = ExecuteCtx(limitedCtx, fn)
78+
} else {
79+
err = ExecuteCtx(limitedCtx, fn, tc.id)
80+
}
81+
82+
if !errors.Is(err, tc.wantErr) {
83+
t.Errorf("got error %v, want %v", err, tc.wantErr)
84+
}
85+
})
86+
}
87+
}
88+
2689
// TestExecuteTx verifies transaction retry using the classic
2790
// example of write skew in bank account balance transfers.
2891
func TestExecuteTx(t *testing.T) {

go.mod

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
module github.com/cockroachdb/cockroach-go/v2
22

3-
go 1.19
3+
go 1.23
44

55
require (
6-
github.com/gofrs/flock v0.8.1
6+
github.com/gofrs/flock v0.12.1
77
github.com/jackc/pgx/v4 v4.18.3
8-
github.com/jackc/pgx/v5 v5.5.2
9-
github.com/jmoiron/sqlx v1.3.5
10-
github.com/lib/pq v1.10.6
11-
github.com/stretchr/testify v1.8.1
8+
github.com/jackc/pgx/v5 v5.7.2
9+
github.com/jmoiron/sqlx v1.4.0
10+
github.com/lib/pq v1.10.9
11+
github.com/stretchr/testify v1.10.0
1212
gopkg.in/yaml.v3 v3.0.1
13-
gorm.io/driver/postgres v1.3.5
14-
gorm.io/gorm v1.23.5
13+
gorm.io/driver/postgres v1.5.11
14+
gorm.io/gorm v1.25.12
1515
)
1616

1717
require (
@@ -21,16 +21,16 @@ require (
2121
github.com/jackc/pgio v1.0.0 // indirect
2222
github.com/jackc/pgpassfile v1.0.0 // indirect
2323
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
24-
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
24+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
2525
github.com/jackc/pgtype v1.14.3 // indirect
2626
github.com/jackc/puddle v1.3.0 // indirect
27-
github.com/jackc/puddle/v2 v2.2.1 // indirect
27+
github.com/jackc/puddle/v2 v2.2.2 // indirect
2828
github.com/jinzhu/inflection v1.0.0 // indirect
29-
github.com/jinzhu/now v1.1.4 // indirect
29+
github.com/jinzhu/now v1.1.5 // indirect
3030
github.com/pmezard/go-difflib v1.0.0 // indirect
3131
github.com/rogpeppe/go-internal v1.9.0 // indirect
32-
golang.org/x/crypto v0.22.0 // indirect
33-
golang.org/x/sync v0.7.0 // indirect
34-
golang.org/x/sys v0.19.0 // indirect
35-
golang.org/x/text v0.14.0 // indirect
32+
golang.org/x/crypto v0.31.0 // indirect
33+
golang.org/x/sync v0.10.0 // indirect
34+
golang.org/x/sys v0.28.0 // indirect
35+
golang.org/x/text v0.21.0 // indirect
3636
)

0 commit comments

Comments
 (0)