Skip to content

Commit 81016d5

Browse files
committed
crdb: abort retries when context is cancelled in ExecuteCtx()
1 parent 10c2916 commit 81016d5

File tree

2 files changed

+55
-18
lines changed

2 files changed

+55
-18
lines changed

crdb/tx.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,24 @@ 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+
103108
// ExecuteCtx runs fn and retries it as needed, respecting a maximum retry count
104109
// obtained from the context. It is used to add configurable retry handling to
105110
// the execution of a single statement. If a multi-statement transaction is
106111
// being run, use ExecuteTx instead.
107112
//
108113
// The maximum number of retries can be configured using WithMaxRetries(ctx, n).
109-
// If the number of retries is exhausted and the last attempt resulted in a
110-
// retryable error, ExecuteCtx returns a max retries exceeded error wrapping
111-
// the last retryable error encountered.
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.
112121
//
113122
// As with Execute, retry handling for individual statements (implicit transactions)
114123
// is usually performed automatically on the CockroachDB SQL gateway, making use
@@ -123,19 +132,24 @@ func Execute(fn func() error) (err error) {
123132
// `Cause() error` (github.com/pkg/errors) or `Unwrap() error` (Go 1.13+).
124133
// For example:
125134
//
126-
// crdb.ExecuteCtx(ctx, func () error {
127-
// rows, err := db.QueryContext(ctx, "SELECT ...")
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)
128138
// if err != nil {
129139
// return fmt.Errorf("scanning row: %w", err) // uses %w for proper error wrapping
130140
// }
131141
// defer rows.Close()
132142
// // ...
133143
// return nil
134-
// })
135-
func ExecuteCtx(ctx context.Context, fn func(ctx context.Context) error) (err error) {
144+
// }, userID)
145+
func ExecuteCtx(ctx context.Context, fn ExecuteCtxFunc, args ...interface{}) (err error) {
136146
maxRetries := numRetriesFromContext(ctx)
137147
for n := 0; n <= maxRetries; n++ {
138-
err = fn(ctx)
148+
if err := ctx.Err(); err != nil {
149+
return err
150+
}
151+
152+
err = fn(ctx, args...)
139153
if err == nil || !errIsRetryable(err) {
140154
return err
141155
}

crdb/tx_test.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ 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

2627
// TestExecuteCtx verifies that ExecuteCtx correctly handles different retry limits
27-
// when executing database operations. It tests both successful operations and
28-
// retry behavior.
28+
// and context cancellation when executing database operations.
2929
//
3030
// TODO(seanc@): Add test cases that force retryable errors by simulating
3131
// transaction conflicts or network failures. Consider using the same write skew
@@ -44,20 +44,43 @@ func TestExecuteCtx(t *testing.T) {
4444
name string
4545
maxRetries int
4646
id int
47+
withCancel bool
48+
wantErr error
4749
}{
48-
{"no retries", 0, 0},
49-
{"single retry", 1, 1},
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
5064
}
5165

5266
for _, tc := range testCases {
5367
t.Run(tc.name, func(t *testing.T) {
5468
limitedCtx := WithMaxRetries(ctx, tc.maxRetries)
55-
err := ExecuteCtx(limitedCtx, func(ctx context.Context) error {
56-
_, err := db.ExecContext(ctx, `INSERT INTO test_retry VALUES ($1)`, tc.id)
57-
return err
58-
})
59-
if err != nil {
60-
t.Errorf("expected success with retry limit %d, got: %v", tc.maxRetries, err)
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)
6184
}
6285
})
6386
}

0 commit comments

Comments
 (0)