Skip to content

Commit 9f77b52

Browse files
authored
Merge pull request #10703 from yyforyongyu/10697-review-fixes
Fix `sqldb/v2` regressions
2 parents 80572fe + 7074419 commit 9f77b52

16 files changed

Lines changed: 680 additions & 103 deletions

sqldb/v2/config.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import (
77
)
88

99
const (
10-
// defaultMaxConns is the number of permitted active and idle
10+
// DefaultSqliteMaxConns is the default number of maximum open
11+
// connections for SQLite. SQLite only supports a single writer, so a
12+
// low default reduces contention on the busy_timeout and limits
13+
// resource usage.
14+
DefaultSqliteMaxConns = 2
15+
16+
// DefaultPostgresMaxConns is the number of permitted active and idle
1117
// connections. We want to limit this so it isn't unlimited. We use the
1218
// same value for the number of idle connections as, this can speed up
1319
// queries given a new connection doesn't need to be established each
1420
// time.
15-
defaultMaxConns = 25
21+
DefaultPostgresMaxConns = 25
1622

1723
// defaultMaxIdleConns is the number of permitted idle connections.
1824
defaultMaxIdleConns = 6
@@ -62,6 +68,25 @@ func (s *SqliteConfig) busyTimeoutMs() int64 {
6268
return DefaultSqliteBusyTimeout.Milliseconds()
6369
}
6470

71+
// MaxConns returns the effective maximum number of SQLite connections.
72+
func (s *SqliteConfig) MaxConns() int {
73+
if s.MaxConnections > 0 {
74+
return s.MaxConnections
75+
}
76+
77+
return DefaultSqliteMaxConns
78+
}
79+
80+
// MaxIdleConns returns the effective maximum number of idle SQLite
81+
// connections.
82+
func (s *SqliteConfig) MaxIdleConns() int {
83+
if s.MaxIdleConnections > 0 {
84+
return s.MaxIdleConnections
85+
}
86+
87+
return s.MaxConns()
88+
}
89+
6590
// Validate checks that the SqliteConfig values are valid.
6691
func (p *SqliteConfig) Validate() error {
6792
if err := p.QueryConfig.Validate(true); err != nil {

sqldb/v2/config_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package sqldb
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
// TestSqliteConfigMaxConns verifies that SQLite keeps the low default
10+
// connection limit unless the caller overrides it explicitly.
11+
func TestSqliteConfigMaxConns(t *testing.T) {
12+
t.Parallel()
13+
14+
testCases := []struct {
15+
name string
16+
maxConns int
17+
expectedConn int
18+
}{
19+
{
20+
name: "default limit",
21+
expectedConn: DefaultSqliteMaxConns,
22+
},
23+
{
24+
name: "explicit limit",
25+
maxConns: 7,
26+
expectedConn: 7,
27+
},
28+
}
29+
30+
for _, testCase := range testCases {
31+
testCase := testCase
32+
33+
t.Run(testCase.name, func(t *testing.T) {
34+
t.Parallel()
35+
36+
cfg := &SqliteConfig{
37+
MaxConnections: testCase.maxConns,
38+
}
39+
40+
require.Equal(t, testCase.expectedConn, cfg.MaxConns())
41+
})
42+
}
43+
}
44+
45+
// TestSqliteConfigMaxIdleConns verifies that SQLite defaults its idle
46+
// connections to the open connection limit unless the caller overrides it.
47+
func TestSqliteConfigMaxIdleConns(t *testing.T) {
48+
t.Parallel()
49+
50+
testCases := []struct {
51+
name string
52+
maxConns int
53+
maxIdleConns int
54+
expectedIdleConn int
55+
}{
56+
{
57+
name: "default idle limit",
58+
expectedIdleConn: DefaultSqliteMaxConns,
59+
},
60+
{
61+
name: "inherits explicit open limit",
62+
maxConns: 4,
63+
expectedIdleConn: 4,
64+
},
65+
{
66+
name: "explicit idle limit",
67+
maxConns: 4,
68+
maxIdleConns: 3,
69+
expectedIdleConn: 3,
70+
},
71+
}
72+
73+
for _, testCase := range testCases {
74+
testCase := testCase
75+
76+
t.Run(testCase.name, func(t *testing.T) {
77+
t.Parallel()
78+
79+
cfg := &SqliteConfig{
80+
MaxConnections: testCase.maxConns,
81+
MaxIdleConnections: testCase.maxIdleConns,
82+
}
83+
84+
require.Equal(t, testCase.expectedIdleConn,
85+
cfg.MaxIdleConns())
86+
})
87+
}
88+
}

sqldb/v2/interfaces.go

Lines changed: 72 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,12 @@ func ReadTxOpt() TxOptions {
8484
}
8585
}
8686

87-
// BaseQuerier is a generic interface that represents the base methods that any
88-
// database backend implementation which uses a Querier for its operations must
89-
// implement.
90-
type BaseQuerier interface {
91-
// Backend returns the type of the database backend used.
92-
Backend() BackendType
93-
}
94-
9587
// BatchedTx is a generic interface that represents the ability to execute
9688
// several operations to a given storage interface in a single atomic
9789
// transaction. Typically, Q here will be some subset of the main sqlc.Querier
9890
// interface allowing it to only depend on the routines it needs to implement
9991
// any additional business logic.
100-
type BatchedTx[Q BaseQuerier] interface {
92+
type BatchedTx[Q any] interface {
10193
// ExecTx will execute the passed txBody, operating upon generic
10294
// parameter Q (usually a storage interface) in a single transaction.
10395
//
@@ -137,6 +129,9 @@ type BatchedQuerier interface {
137129
// BeginTx creates a new database transaction given the set of
138130
// transaction options.
139131
BeginTx(ctx context.Context, options TxOptions) (*sql.Tx, error)
132+
133+
// Backend returns the type of the database backend used.
134+
Backend() BackendType
140135
}
141136

142137
// txExecutorOptions is a struct that holds the options for the transaction
@@ -158,12 +153,6 @@ func defaultTxExecutorOptions() *txExecutorOptions {
158153
}
159154
}
160155

161-
// randRetryDelay returns a random retry delay between 0 and the configured max
162-
// delay.
163-
func (t *txExecutorOptions) randRetryDelay() time.Duration {
164-
return time.Duration(rand.Int63n(int64(t.maxRetryDelay))) //nolint:gosec
165-
}
166-
167156
// TxExecutorOption is a functional option that allows us to pass in optional
168157
// argument when creating the executor.
169158
type TxExecutorOption func(*txExecutorOptions)
@@ -188,18 +177,22 @@ func WithTxRetryDelay(delay time.Duration) TxExecutorOption {
188177
// query a type needs to run under a database transaction, and also the set of
189178
// options for that transaction. The QueryCreator is used to create a query
190179
// given a database transaction created by the BatchedQuerier.
191-
type TransactionExecutor[Query BaseQuerier] struct {
180+
type TransactionExecutor[Query any] struct {
192181
BatchedQuerier
193182

194183
createQuery QueryCreator[Query]
195184

196185
opts *txExecutorOptions
197186
}
198187

188+
// A compile-time assertion to ensure TransactionExecutor satisfies the
189+
// batched transaction interface.
190+
var _ BatchedTx[any] = (*TransactionExecutor[any])(nil)
191+
199192
// NewTransactionExecutor creates a new instance of a TransactionExecutor given
200193
// a Querier query object and a concrete type for the type of transactions the
201194
// Querier understands.
202-
func NewTransactionExecutor[Querier BaseQuerier](db BatchedQuerier,
195+
func NewTransactionExecutor[Querier any](db BatchedQuerier,
203196
createQuery QueryCreator[Querier],
204197
opts ...TxExecutorOption) *TransactionExecutor[Querier] {
205198

@@ -215,6 +208,11 @@ func NewTransactionExecutor[Querier BaseQuerier](db BatchedQuerier,
215208
}
216209
}
217210

211+
// Backend returns the type of database backend used by the executor.
212+
func (t *TransactionExecutor[Q]) Backend() BackendType {
213+
return t.BatchedQuerier.Backend()
214+
}
215+
218216
// randRetryDelay returns a random retry delay between -50% and +50% of the
219217
// configured delay that is doubled for each attempt and capped at a max value.
220218
func randRetryDelay(initialRetryDelay, maxRetryDelay time.Duration,
@@ -268,6 +266,55 @@ type RollbackTx func(tx Tx) error
268266
// the delay before the next retry.
269267
type OnBackoff func(retry int, delay time.Duration)
270268

269+
// executeTxAttempt runs a single transaction attempt and reports whether the
270+
// caller should retry it.
271+
func executeTxAttempt(tx Tx, txBody TxBody, rollbackTx RollbackTx,
272+
waitBeforeRetry func(int) bool, attempt int) (bool, error) {
273+
274+
// Rollback is safe to call even if the tx is already closed, so if the tx
275+
// commits successfully, this is a no-op.
276+
defer func() {
277+
_ = tx.Rollback()
278+
}()
279+
280+
if bodyErr := txBody(tx); bodyErr != nil {
281+
log.Tracef("Error in txBody: %v", bodyErr)
282+
283+
// Roll back the transaction, then attempt a random backoff and try
284+
// again if the error was a serialization error.
285+
if err := rollbackTx(tx); err != nil {
286+
return false, MapSQLError(err)
287+
}
288+
289+
dbErr := MapSQLError(bodyErr)
290+
if IsSerializationOrDeadlockError(dbErr) {
291+
return waitBeforeRetry(attempt), dbErr
292+
}
293+
294+
return false, dbErr
295+
}
296+
297+
// Commit transaction.
298+
if commitErr := tx.Commit(); commitErr != nil {
299+
log.Tracef("Failed to commit tx: %v", commitErr)
300+
301+
// Roll back the transaction, then attempt a random backoff and try
302+
// again if the error was a serialization error.
303+
if err := rollbackTx(tx); err != nil {
304+
return false, MapSQLError(err)
305+
}
306+
307+
dbErr := MapSQLError(commitErr)
308+
if IsSerializationOrDeadlockError(dbErr) {
309+
return waitBeforeRetry(attempt), dbErr
310+
}
311+
312+
return false, dbErr
313+
}
314+
315+
return false, nil
316+
}
317+
271318
// ExecuteSQLTransactionWithRetry is a helper function that executes a
272319
// transaction with retry logic. It will retry the transaction if it fails with
273320
// a serialization error. The function will return an error if the transaction
@@ -316,51 +363,15 @@ func ExecuteSQLTransactionWithRetry(ctx context.Context, makeTx MakeTx,
316363
return dbErr
317364
}
318365

319-
// Rollback is safe to call even if the tx is already closed,
320-
// so if the tx commits successfully, this is a no-op.
321-
defer func() {
322-
_ = tx.Rollback()
323-
}()
324-
325-
if bodyErr := txBody(tx); bodyErr != nil {
326-
log.Tracef("Error in txBody: %v", bodyErr)
327-
328-
// Roll back the transaction, then attempt a random
329-
// backoff and try again if the error was a
330-
// serialization error.
331-
if err := rollbackTx(tx); err != nil {
332-
return MapSQLError(err)
333-
}
334-
335-
dbErr := MapSQLError(bodyErr)
336-
if IsSerializationOrDeadlockError(dbErr) {
337-
if waitBeforeRetry(i) {
338-
continue
339-
}
340-
}
341-
342-
return dbErr
366+
retry, err := executeTxAttempt(
367+
tx, txBody, rollbackTx, waitBeforeRetry, i,
368+
)
369+
if retry {
370+
// Transient serialization error, discard this attempt and retry.
371+
continue
343372
}
344-
345-
// Commit transaction.
346-
if commitErr := tx.Commit(); commitErr != nil {
347-
log.Tracef("Failed to commit tx: %v", commitErr)
348-
349-
// Roll back the transaction, then attempt a random
350-
// backoff and try again if the error was a
351-
// serialization error.
352-
if err := rollbackTx(tx); err != nil {
353-
return MapSQLError(err)
354-
}
355-
356-
dbErr := MapSQLError(commitErr)
357-
if IsSerializationOrDeadlockError(dbErr) {
358-
if waitBeforeRetry(i) {
359-
continue
360-
}
361-
}
362-
363-
return dbErr
373+
if err != nil {
374+
return err
364375
}
365376

366377
return nil

sqldb/v2/interfaces_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package sqldb
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// testQuerier is a minimal query wrapper used to instantiate the generic
12+
// transaction executor in tests.
13+
type testQuerier struct {
14+
}
15+
16+
// testBatchedQuerier is a minimal BatchedQuerier implementation used to verify
17+
// that TransactionExecutor forwards backend identity.
18+
type testBatchedQuerier struct {
19+
backend BackendType
20+
}
21+
22+
// BeginTx is a stub implementation used to satisfy the BatchedQuerier
23+
// interface in tests.
24+
func (t testBatchedQuerier) BeginTx(context.Context,
25+
TxOptions) (*sql.Tx, error) {
26+
27+
return nil, nil
28+
}
29+
30+
// Backend returns the backend type used by the test batched querier.
31+
func (t testBatchedQuerier) Backend() BackendType {
32+
return t.backend
33+
}
34+
35+
// TestTransactionExecutorBackend verifies that the executor forwards the
36+
// backend type from its batched querier.
37+
func TestTransactionExecutorBackend(t *testing.T) {
38+
t.Parallel()
39+
40+
executor := NewTransactionExecutor[testQuerier](
41+
testBatchedQuerier{backend: BackendTypePostgres},
42+
func(*sql.Tx) testQuerier {
43+
return testQuerier{}
44+
},
45+
)
46+
47+
require.Equal(t, BackendTypePostgres, executor.Backend())
48+
}

0 commit comments

Comments
 (0)