├── .gitignore ├── crdb ├── crdbpgxv5 │ ├── README.md │ ├── pgx.go │ └── pgx_test.go ├── crdbpgx │ ├── README.md │ ├── pgx.go │ └── pgx_test.go ├── error_test.go ├── crdbsqlx │ ├── sqlx.go │ └── sqlx_test.go ├── crdbgorm │ ├── gorm.go │ └── gorm_test.go ├── error.go ├── README.md ├── testing_util.go ├── common.go ├── tx_test.go ├── retry_test.go ├── retry.go └── tx.go ├── README.md ├── go.mod ├── .github └── workflows │ └── ci.yaml ├── testserver ├── README.md ├── testservernode.go ├── version │ └── version.go ├── tenant.go ├── binaries.go └── testserver_test.go ├── cmd └── download-crdb-binaries │ └── main.go ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | *.test 4 | testserver/cockroach-data/ 5 | testserver/temp_binaries/ 6 | -------------------------------------------------------------------------------- /crdb/crdbpgxv5/README.md: -------------------------------------------------------------------------------- 1 | `crdbpgxv5` is a wrapper around the logic for issuing SQL transactions which 2 | performs retries (as required by CockroachDB) when using 3 | [`github.com/jackc/pgx`](https://github.com/jackc/pgx) in standalone-library 4 | mode. `crdbpgxv5` only supports `pgx/v5`. 5 | 6 | Note: use `crdbpgx` for `pgx/v4` support 7 | 8 | If you're using pgx just as a driver for the standard `database/sql` package, 9 | use the parent `crdb` package instead. 10 | -------------------------------------------------------------------------------- /crdb/crdbpgx/README.md: -------------------------------------------------------------------------------- 1 | `crdbpgx` is a wrapper around the logic for issuing SQL transactions which 2 | performs retries (as required by CockroachDB) when using 3 | [`github.com/jackc/pgx`](https://github.com/jackc/pgx) in standalone-library 4 | mode. pgx versions below v4 are not supported. 5 | 6 | Note: use `crdbpgxv5` for `pgx/v5` support 7 | 8 | If you're using pgx just as a driver for the standard `database/sql` package, 9 | use the parent `crdb` package instead. 10 | -------------------------------------------------------------------------------- /crdb/error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Cockroach Authors. 2 | // 3 | // Use of this software is governed by the Business Source License 4 | // included in the file licenses/BSL.txt. 5 | // 6 | // As of the Change Date specified in that file, in accordance with 7 | // the Business Source License, use of this software will be governed 8 | // by the Apache License, Version 2.0, included in the file 9 | // licenses/APL.txt. 10 | 11 | package crdb 12 | 13 | import ( 14 | "errors" 15 | "fmt" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func TestMaxRetriesExceededError(t *testing.T) { 21 | origError := fmt.Errorf("root error") 22 | err := newMaxRetriesExceededError(origError, 10) 23 | if !strings.HasPrefix(err.Error(), "retrying txn failed after 10 attempts.") { 24 | t.Fatalf("expected txn retry error message") 25 | } 26 | if !errors.Is(err, origError) { 27 | t.Fatal("expected to find root error cause") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CockroachDB Go Helpers [![Go Reference](https://pkg.go.dev/badge/github.com/cockroachdb/cockroach-go/v2/crdb.svg)](https://pkg.go.dev/github.com/cockroachdb/cockroach-go/v2/crdb) 2 | 3 | This project contains helpers for CockroachDB users writing in Go: 4 | - `crdb` and its subpackages provide wrapper functions for retrying transactions that fail 5 | due to serialization errors. It is intended for use within any Go application. See 6 | `crdb/README.md` for more details. 7 | - `testserver` provides functions for starting and connecting to a locally running instance of 8 | CockroachDB. It is intended for use in test code. 9 | 10 | ## Prerequisites 11 | 12 | The current release (v2) of this library requires Go modules. 13 | 14 | You can import it in your code using: 15 | 16 | ``` 17 | import ( 18 | "github.com/cockroachdb/cockroach-go/v2/crdb" 19 | "github.com/cockroachdb/cockroach-go/v2/testserver" 20 | ) 21 | ``` 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cockroachdb/cockroach-go/v2 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gofrs/flock v0.12.1 7 | github.com/jackc/pgx/v4 v4.18.3 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 12 | gopkg.in/yaml.v3 v3.0.1 13 | gorm.io/driver/postgres v1.5.11 14 | gorm.io/gorm v1.25.12 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 20 | github.com/jackc/pgconn v1.14.3 // indirect 21 | github.com/jackc/pgio v1.0.0 // indirect 22 | github.com/jackc/pgpassfile v1.0.0 // indirect 23 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 24 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 25 | github.com/jackc/pgtype v1.14.3 // indirect 26 | github.com/jackc/puddle v1.3.0 // indirect 27 | github.com/jackc/puddle/v2 v2.2.2 // indirect 28 | github.com/jinzhu/inflection v1.0.0 // indirect 29 | github.com/jinzhu/now v1.1.5 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/rogpeppe/go-internal v1.9.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 36 | ) 37 | -------------------------------------------------------------------------------- /crdb/crdbsqlx/sqlx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbsqlx 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | 21 | "github.com/cockroachdb/cockroach-go/v2/crdb" 22 | "github.com/jmoiron/sqlx" 23 | _ "github.com/lib/pq" 24 | ) 25 | 26 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 27 | // non-retryable failures, the transaction is aborted and rolled back; on 28 | // success, the transaction is committed. 29 | // 30 | // See crdb.ExecuteTx() for more information. 31 | func ExecuteTx( 32 | ctx context.Context, db *sqlx.DB, opts *sql.TxOptions, fn func(*sqlx.Tx) error, 33 | ) error { 34 | tx, err := db.BeginTxx(ctx, opts) 35 | if err != nil { 36 | return err 37 | } 38 | return crdb.ExecuteInTx(ctx, sqlxTxAdapter{tx}, func() error { return fn(tx) }) 39 | } 40 | 41 | // sqlxTxAdapter adapts a *sqlx.tx to a crdb.Tx. 42 | type sqlxTxAdapter struct { 43 | *sqlx.Tx 44 | } 45 | 46 | var _ crdb.Tx = sqlxTxAdapter{} 47 | 48 | func (s sqlxTxAdapter) Exec(ctx context.Context, query string, args ...interface{}) error { 49 | _, err := s.Tx.ExecContext(ctx, query, args...) 50 | return err 51 | } 52 | 53 | func (s sqlxTxAdapter) Commit(ctx context.Context) error { 54 | return s.Tx.Commit() 55 | } 56 | 57 | func (s sqlxTxAdapter) Rollback(ctx context.Context) error { 58 | return s.Tx.Rollback() 59 | } 60 | -------------------------------------------------------------------------------- /crdb/crdbgorm/gorm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbgorm 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | 21 | "github.com/cockroachdb/cockroach-go/v2/crdb" 22 | "gorm.io/gorm" 23 | ) 24 | 25 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 26 | // non-retryable failures, the transaction is aborted and rolled back; on 27 | // success, the transaction is committed. 28 | // 29 | // See crdb.ExecuteTx() for more information. 30 | func ExecuteTx( 31 | ctx context.Context, db *gorm.DB, opts *sql.TxOptions, fn func(tx *gorm.DB) error, 32 | ) error { 33 | tx := db.WithContext(ctx).Begin(opts) 34 | if tx.Error != nil { 35 | return tx.Error 36 | } 37 | return crdb.ExecuteInTx(ctx, gormTxAdapter{tx}, func() error { return fn(tx) }) 38 | } 39 | 40 | // gormTxAdapter adapts a *gorm.DB to a crdb.Tx. 41 | type gormTxAdapter struct { 42 | db *gorm.DB 43 | } 44 | 45 | var _ crdb.Tx = gormTxAdapter{} 46 | 47 | // Exec is part of the crdb.Tx interface. 48 | func (tx gormTxAdapter) Exec(_ context.Context, q string, args ...interface{}) error { 49 | return tx.db.Exec(q, args...).Error 50 | } 51 | 52 | // Commit is part of the crdb.Tx interface. 53 | func (tx gormTxAdapter) Commit(_ context.Context) error { 54 | return tx.db.Commit().Error 55 | } 56 | 57 | // Rollback is part of the crdb.Tx interface. 58 | func (tx gormTxAdapter) Rollback(_ context.Context) error { 59 | return tx.db.Rollback().Error 60 | } 61 | -------------------------------------------------------------------------------- /crdb/crdbpgx/pgx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbpgx 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/cockroachdb/cockroach-go/v2/crdb" 21 | "github.com/jackc/pgx/v4" 22 | ) 23 | 24 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 25 | // non-retryable failures, the transaction is aborted and rolled back; on 26 | // success, the transaction is committed. 27 | // 28 | // See crdb.ExecuteTx() for more information. 29 | // 30 | // conn can be a pgx.Conn or a pgxpool.Pool. 31 | func ExecuteTx( 32 | ctx context.Context, conn Conn, txOptions pgx.TxOptions, fn func(pgx.Tx) error, 33 | ) error { 34 | tx, err := conn.BeginTx(ctx, txOptions) 35 | if err != nil { 36 | return err 37 | } 38 | return crdb.ExecuteInTx(ctx, pgxTxAdapter{tx}, func() error { return fn(tx) }) 39 | } 40 | 41 | // Conn abstracts pgx transactions creators: pgx.Conn and pgxpool.Pool. 42 | type Conn interface { 43 | Begin(context.Context) (pgx.Tx, error) 44 | BeginTx(context.Context, pgx.TxOptions) (pgx.Tx, error) 45 | } 46 | 47 | type pgxTxAdapter struct { 48 | tx pgx.Tx 49 | } 50 | 51 | var _ crdb.Tx = pgxTxAdapter{} 52 | 53 | func (tx pgxTxAdapter) Commit(ctx context.Context) error { 54 | return tx.tx.Commit(ctx) 55 | } 56 | 57 | func (tx pgxTxAdapter) Rollback(ctx context.Context) error { 58 | return tx.tx.Rollback(ctx) 59 | } 60 | 61 | // Exec is part of the crdb.Tx interface. 62 | func (tx pgxTxAdapter) Exec(ctx context.Context, q string, args ...interface{}) error { 63 | _, err := tx.tx.Exec(ctx, q, args...) 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /crdb/crdbpgxv5/pgx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbpgx 16 | 17 | import ( 18 | "context" 19 | "github.com/jackc/pgx/v5" 20 | 21 | "github.com/cockroachdb/cockroach-go/v2/crdb" 22 | ) 23 | 24 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 25 | // non-retryable failures, the transaction is aborted and rolled back; on 26 | // success, the transaction is committed. 27 | // 28 | // See crdb.ExecuteTx() for more information. 29 | // 30 | // conn can be a pgx.Conn or a pgxpool.Pool. 31 | func ExecuteTx( 32 | ctx context.Context, conn Conn, txOptions pgx.TxOptions, fn func(pgx.Tx) error, 33 | ) error { 34 | tx, err := conn.BeginTx(ctx, txOptions) 35 | if err != nil { 36 | return err 37 | } 38 | return crdb.ExecuteInTx(ctx, pgxTxAdapter{tx}, func() error { return fn(tx) }) 39 | } 40 | 41 | // Conn abstracts pgx transactions creators: pgx.Conn and pgxpool.Pool. 42 | type Conn interface { 43 | Begin(context.Context) (pgx.Tx, error) 44 | BeginTx(context.Context, pgx.TxOptions) (pgx.Tx, error) 45 | } 46 | 47 | type pgxTxAdapter struct { 48 | tx pgx.Tx 49 | } 50 | 51 | var _ crdb.Tx = pgxTxAdapter{} 52 | 53 | func (tx pgxTxAdapter) Commit(ctx context.Context) error { 54 | return tx.tx.Commit(ctx) 55 | } 56 | 57 | func (tx pgxTxAdapter) Rollback(ctx context.Context) error { 58 | return tx.tx.Rollback(ctx) 59 | } 60 | 61 | // Exec is part of the crdb.Tx interface. 62 | func (tx pgxTxAdapter) Exec(ctx context.Context, q string, args ...interface{}) error { 63 | _, err := tx.tx.Exec(ctx, q, args...) 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | # This job aggregates all matrix results and is used for a GitHub required status check. 11 | test_results: 12 | if: ${{ always() }} 13 | runs-on: ubuntu-latest 14 | name: Test Results 15 | needs: [build_and_test] 16 | steps: 17 | - run: | 18 | result="${{ needs.build_and_test.result }}" 19 | if [[ $result == "success" || $result == "skipped" ]]; then 20 | exit 0 21 | else 22 | exit 1 23 | fi 24 | 25 | build_and_test: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | go: 30 | - "1.23" 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | 35 | - name: Set up Go ${{ matrix.go }} 36 | uses: actions/setup-go@v2 37 | with: 38 | go-version: ${{ matrix.go }} 39 | 40 | - name: Install dependencies 41 | run: | 42 | go install github.com/kisielk/errcheck@latest 43 | go install github.com/mdempsky/unconvert@latest 44 | go install honnef.co/go/tools/cmd/staticcheck@latest 45 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 46 | 47 | - name: Build 48 | run: go build -v ./... 49 | 50 | - name: Test 51 | run: go test -v ./... 52 | 53 | - name: Race 54 | run: go test -p 1 -v ./... 55 | 56 | - name: gofmt 57 | run: gofmt -s -d -l . 58 | 59 | - name: vet 60 | run: | 61 | ! go vet -vettool=$(which shadow) ./... 2>&1 | \ 62 | grep -vF 'declaration of "err" shadows declaration at' | \ 63 | grep -vF "# github.com/cockroachdb/cockroach-go/v2/testserver" | \ 64 | grep -vF "# [github.com/cockroachdb/cockroach-go/v2/testserver_test]" 65 | 66 | - name: License checks 67 | run: | 68 | ! git grep -lE '^// Author' -- '*.go' 69 | ! git grep -LE '^// Copyright' -- '*.go' 70 | 71 | - name: errchk 72 | run: errcheck -ignore "Close|Init|AutoMigrate" ./... 73 | 74 | - name: staticcheck 75 | run: staticcheck ./... 76 | -------------------------------------------------------------------------------- /testserver/README.md: -------------------------------------------------------------------------------- 1 | # cockroach-go Testserver 2 | 3 | The `testserver` package helps running cockroachDB binary with tests. It 4 | automatically downloads the latest stable cockroach binary for your runtimeOS, 5 | or attempts to run "cockroach" from your PATH. 6 | 7 | ### Example 8 | To run the test server, call `NewTestServer(opts)` and with test server options. 9 | 10 | Here's an example of starting a test server without server options (i.e. in `Insecure` 11 | mode). 12 | 13 | ```go 14 | import "github.com/cockroachdb/cockroach-go/v2/testserver" 15 | import "testing" 16 | import "time" 17 | 18 | func TestRunServer(t *testing.T) { 19 | ts, err := testserver.NewTestServer() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer ts.Stop() 24 | 25 | db, err := sql.Open("postgres", ts.PGURL().String()) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | } 30 | ``` 31 | 32 | **Note: please always use `testserver.NewTestServer()` to start a test server. Never use 33 | `testserver.Start()`.** 34 | 35 | ### Test Server Options 36 | 37 | The default configuration is : 38 | 39 | - in insecure mode, so not using TLS certificates to encrypt network; 40 | - storing data to memory, with 20% of hard drive space assigned to the node; 41 | - auto-downloading the latest stable release of cockroachDB. 42 | 43 | You can also choose from the following options and pass them to `testserver. 44 | NewTestServer()`. 45 | 46 | - **Secure Mode**: run a secure multi-node cluster locally, using TLS certificates to encrypt network communication. 47 | See also https://www.cockroachlabs.com/docs/stable/secure-a-cluster.html. 48 | - Usage: ```NewTestServer(testserver.SecureOpt())``` 49 | - **Set Root User's Password**: set the given password for the root user for the 50 | PostgreSQL server, so to avoid having to use client certificates. This option can 51 | only be passed under secure mode. 52 | - Usage: ```NewTestServer(testserver.RootPasswordOpt 53 | (your_password))``` 54 | - **Store On Disk**: store the database to the local disk. By default, the database is 55 | saved at `/tmp/cockroach-testserverxxxxxxxx`, with randomly generated `xxxxxxxx` 56 | postfix. 57 | - Usage: ```NewTestServer(testserver.StoreOnDiskOpt())``` 58 | - **Set Memory Allocation for Databse Storage**: set the maximum percentage of 59 | total memory space assigned to store the database. 60 | See also https://www.cockroachlabs. com/docs/stable/cockroach-start.html. 61 | - Usage: 62 | ```NewTestServer(testserver.SetStoreMemSizeOpt(0.3))``` 63 | 64 | ### Test Server for Multi Tenants 65 | The usage of test server as a tenant server is still under development. Please 66 | check `testserver/tenant.go` for more information. 67 | -------------------------------------------------------------------------------- /crdb/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import "fmt" 18 | 19 | type txError struct { 20 | cause error 21 | } 22 | 23 | // Error implements the error interface. 24 | func (e *txError) Error() string { return e.cause.Error() } 25 | 26 | // Cause implements the pkg/errors causer interface. 27 | func (e *txError) Cause() error { return e.cause } 28 | 29 | // Unwrap implements the go error causer interface. 30 | func (e *txError) Unwrap() error { return e.cause } 31 | 32 | // AmbiguousCommitError represents an error that left a transaction in an 33 | // ambiguous state: unclear if it committed or not. 34 | type AmbiguousCommitError struct { 35 | txError 36 | } 37 | 38 | func newAmbiguousCommitError(err error) *AmbiguousCommitError { 39 | return &AmbiguousCommitError{txError{cause: err}} 40 | } 41 | 42 | // MaxRetriesExceededError represents an error caused by retying the transaction 43 | // too many times, without it ever succeeding. 44 | type MaxRetriesExceededError struct { 45 | txError 46 | msg string 47 | } 48 | 49 | func newMaxRetriesExceededError(err error, maxRetries int) *MaxRetriesExceededError { 50 | const msgPattern = "retrying txn failed after %d attempts. original error: %s." 51 | return &MaxRetriesExceededError{ 52 | txError: txError{cause: err}, 53 | msg: fmt.Sprintf(msgPattern, maxRetries, err), 54 | } 55 | } 56 | 57 | // Error implements the error interface. 58 | func (e *MaxRetriesExceededError) Error() string { return e.msg } 59 | 60 | // TxnRestartError represents an error when restarting a transaction. `cause` is 61 | // the error from restarting the txn and `retryCause` is the original error which 62 | // triggered the restart. 63 | type TxnRestartError struct { 64 | txError 65 | retryCause error 66 | msg string 67 | } 68 | 69 | func newTxnRestartError(err error, retryErr error, op string) *TxnRestartError { 70 | const msgPattern = "restarting txn failed. %s " + 71 | "encountered error: %s. Original error: %s." 72 | return &TxnRestartError{ 73 | txError: txError{cause: err}, 74 | retryCause: retryErr, 75 | msg: fmt.Sprintf(msgPattern, op, err, retryErr), 76 | } 77 | } 78 | 79 | // Error implements the error interface. 80 | func (e *TxnRestartError) Error() string { return e.msg } 81 | 82 | // RetryCause returns the error that caused the transaction to be restarted. 83 | func (e *TxnRestartError) RetryCause() error { return e.retryCause } 84 | -------------------------------------------------------------------------------- /crdb/crdbpgxv5/pgx_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbpgx 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "github.com/jackc/pgx/v5" 21 | "github.com/jackc/pgx/v5/pgxpool" 22 | "testing" 23 | 24 | "github.com/cockroachdb/cockroach-go/v2/crdb" 25 | "github.com/cockroachdb/cockroach-go/v2/testserver" 26 | ) 27 | 28 | // TestExecuteTx verifies transaction retry using the classic 29 | // example of write skew in bank account balance transfers. 30 | func TestExecuteTx(t *testing.T) { 31 | ts, err := testserver.NewTestServer() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | ctx := context.Background() 36 | 37 | pool, err := pgxpool.New(ctx, ts.PGURL().String()) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if err := crdb.ExecuteTxGenericTest(ctx, pgxWriteSkewTest{pool: pool}); err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | type pgxWriteSkewTest struct { 48 | pool *pgxpool.Pool 49 | } 50 | 51 | func (t pgxWriteSkewTest) Init(ctx context.Context) error { 52 | initStmt := ` 53 | CREATE DATABASE d; 54 | CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT); 55 | INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); 56 | ` 57 | _, err := t.pool.Exec(ctx, initStmt) 58 | return err 59 | } 60 | 61 | var _ crdb.WriteSkewTest = pgxWriteSkewTest{} 62 | 63 | // ExecuteTx is part of the crdb.WriteSkewTest interface. 64 | func (t pgxWriteSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 65 | return ExecuteTx(ctx, t.pool, pgx.TxOptions{}, func(tx pgx.Tx) error { 66 | return fn(tx) 67 | }) 68 | } 69 | 70 | // GetBalances is part of the crdb.WriteSkewTest interface. 71 | func (t pgxWriteSkewTest) GetBalances(ctx context.Context, txi interface{}) (int, int, error) { 72 | tx := txi.(pgx.Tx) 73 | var rows pgx.Rows 74 | rows, err := tx.Query(ctx, `SELECT balance FROM d.t WHERE acct IN (1, 2);`) 75 | if err != nil { 76 | return 0, 0, err 77 | } 78 | defer rows.Close() 79 | var bal1, bal2 int 80 | balances := []*int{&bal1, &bal2} 81 | i := 0 82 | for ; rows.Next(); i++ { 83 | if err = rows.Scan(balances[i]); err != nil { 84 | return 0, 0, err 85 | } 86 | } 87 | if i != 2 { 88 | return 0, 0, fmt.Errorf("expected two balances; got %d", i) 89 | } 90 | return bal1, bal2, nil 91 | } 92 | 93 | // UpdateBalance is part of the crdb.WriteSkewInterface. 94 | func (t pgxWriteSkewTest) UpdateBalance( 95 | ctx context.Context, txi interface{}, acct, delta int, 96 | ) error { 97 | tx := txi.(pgx.Tx) 98 | _, err := tx.Exec(ctx, `UPDATE d.t SET balance=balance+$1 WHERE acct=$2;`, delta, acct) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /crdb/crdbpgx/pgx_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbpgx 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/cockroachdb/cockroach-go/v2/crdb" 23 | "github.com/cockroachdb/cockroach-go/v2/testserver" 24 | "github.com/jackc/pgx/v4" 25 | "github.com/jackc/pgx/v4/pgxpool" 26 | ) 27 | 28 | // TestExecuteTx verifies transaction retry using the classic 29 | // example of write skew in bank account balance transfers. 30 | func TestExecuteTx(t *testing.T) { 31 | ts, err := testserver.NewTestServer() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | ctx := context.Background() 36 | 37 | pool, err := pgxpool.Connect(ctx, ts.PGURL().String()) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if err := crdb.ExecuteTxGenericTest(ctx, pgxWriteSkewTest{pool: pool}); err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | 47 | type pgxWriteSkewTest struct { 48 | pool *pgxpool.Pool 49 | } 50 | 51 | func (t pgxWriteSkewTest) Init(ctx context.Context) error { 52 | initStmt := ` 53 | CREATE DATABASE d; 54 | CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT); 55 | INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); 56 | ` 57 | _, err := t.pool.Exec(ctx, initStmt) 58 | return err 59 | } 60 | 61 | var _ crdb.WriteSkewTest = pgxWriteSkewTest{} 62 | 63 | // ExecuteTx is part of the crdb.WriteSkewTest interface. 64 | func (t pgxWriteSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 65 | return ExecuteTx(ctx, t.pool, pgx.TxOptions{}, func(tx pgx.Tx) error { 66 | return fn(tx) 67 | }) 68 | } 69 | 70 | // GetBalances is part of the crdb.WriteSkewTest interface. 71 | func (t pgxWriteSkewTest) GetBalances(ctx context.Context, txi interface{}) (int, int, error) { 72 | tx := txi.(pgx.Tx) 73 | var rows pgx.Rows 74 | rows, err := tx.Query(ctx, `SELECT balance FROM d.t WHERE acct IN (1, 2);`) 75 | if err != nil { 76 | return 0, 0, err 77 | } 78 | defer rows.Close() 79 | var bal1, bal2 int 80 | balances := []*int{&bal1, &bal2} 81 | i := 0 82 | for ; rows.Next(); i++ { 83 | if err = rows.Scan(balances[i]); err != nil { 84 | return 0, 0, err 85 | } 86 | } 87 | if i != 2 { 88 | return 0, 0, fmt.Errorf("expected two balances; got %d", i) 89 | } 90 | return bal1, bal2, nil 91 | } 92 | 93 | // UpdateBalance is part of the crdb.WriteSkewInterface. 94 | func (t pgxWriteSkewTest) UpdateBalance( 95 | ctx context.Context, txi interface{}, acct, delta int, 96 | ) error { 97 | tx := txi.(pgx.Tx) 98 | _, err := tx.Exec(ctx, `UPDATE d.t SET balance=balance+$1 WHERE acct=$2;`, delta, acct) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /crdb/crdbsqlx/sqlx_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbsqlx 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/cockroachdb/cockroach-go/v2/crdb" 23 | "github.com/cockroachdb/cockroach-go/v2/testserver" 24 | "github.com/jmoiron/sqlx" 25 | _ "github.com/lib/pq" 26 | ) 27 | 28 | // TestExecuteTx verifies transaction retry using the classic 29 | // example of write skew in bank account balance transfers. 30 | func TestExecuteTx(t *testing.T) { 31 | ts, err := testserver.NewTestServer() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | url := ts.PGURL() 36 | db, err := sqlx.Connect("postgres", url.String()) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if err := ts.WaitForInit(); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | ctx := context.Background() 45 | if err := crdb.ExecuteTxGenericTest(ctx, sqlxConnSkewTest{db: db}); err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | type sqlxConnSkewTest struct { 51 | db *sqlx.DB 52 | } 53 | 54 | func (t sqlxConnSkewTest) Init(ctx context.Context) error { 55 | initStmt := ` 56 | CREATE DATABASE d; 57 | CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT); 58 | INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); 59 | ` 60 | _, err := t.db.ExecContext(ctx, initStmt) 61 | return err 62 | } 63 | 64 | var _ crdb.WriteSkewTest = sqlxConnSkewTest{} 65 | 66 | // ExecuteTx is part of the crdb.WriteSkewTest interface. 67 | func (t sqlxConnSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 68 | return ExecuteTx(ctx, t.db, nil /* txOptions */, func(tx *sqlx.Tx) error { 69 | return fn(tx) 70 | }) 71 | } 72 | 73 | // GetBalances is part of the crdb.WriteSkewTest interface. 74 | func (t sqlxConnSkewTest) GetBalances(ctx context.Context, txi interface{}) (int, int, error) { 75 | tx := txi.(*sqlx.Tx) 76 | rows, err := tx.QueryContext(ctx, `SELECT balance FROM d.t WHERE acct IN (1, 2);`) 77 | if err != nil { 78 | return 0, 0, err 79 | } 80 | defer rows.Close() 81 | balances := []int{} 82 | for rows.Next() { 83 | var bal int 84 | if err = rows.Scan(&bal); err != nil { 85 | return 0, 0, err 86 | } 87 | balances = append(balances, bal) 88 | } 89 | if len(balances) != 2 { 90 | return 0, 0, fmt.Errorf("expected two balances; got %d", len(balances)) 91 | } 92 | return balances[0], balances[1], nil 93 | } 94 | 95 | // UpdateBalance is part of the crdb.WriteSkewInterface. 96 | func (t sqlxConnSkewTest) UpdateBalance( 97 | ctx context.Context, txi interface{}, acct, delta int, 98 | ) error { 99 | tx := txi.(*sqlx.Tx) 100 | _, err := tx.ExecContext(ctx, `UPDATE d.t SET balance=balance+$1 WHERE acct=$2;`, delta, acct) 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /cmd/download-crdb-binaries/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "os" 22 | "runtime" 23 | 24 | "github.com/cockroachdb/cockroach-go/v2/testserver" 25 | ) 26 | 27 | func main() { 28 | var ( 29 | platform = flag.String("platform", runtime.GOOS, "Target platform (linux, darwin, windows)") 30 | arch = flag.String("arch", runtime.GOARCH, "Target architecture (amd64, arm64)") 31 | version = flag.String("version", "unstable", "CockroachDB version to download (use 'unstable' for latest bleeding edge, or specify version like 'v23.1.0')") 32 | output = flag.String("output", "", "Output directory (defaults to temp directory)") 33 | help = flag.Bool("help", false, "Show help") 34 | ) 35 | 36 | flag.Parse() 37 | 38 | if *help { 39 | fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) 40 | fmt.Fprintf(os.Stderr, "Download and extract CockroachDB binaries for specified platform and architecture.\n\n") 41 | fmt.Fprintf(os.Stderr, "Options:\n") 42 | flag.PrintDefaults() 43 | fmt.Fprintf(os.Stderr, "\nSupported platforms: linux, darwin, windows\n") 44 | fmt.Fprintf(os.Stderr, "Supported architectures: amd64, arm64\n") 45 | fmt.Fprintf(os.Stderr, "\nExamples:\n") 46 | fmt.Fprintf(os.Stderr, " %s -platform=linux -arch=amd64 -version=v23.1.0\n", os.Args[0]) 47 | fmt.Fprintf(os.Stderr, " %s -platform=darwin -arch=arm64\n", os.Args[0]) 48 | fmt.Fprintf(os.Stderr, " %s -platform=linux -arch=amd64 -version=unstable\n", os.Args[0]) 49 | os.Exit(0) 50 | } 51 | 52 | // Validate platform 53 | switch *platform { 54 | case "linux", "darwin", "windows": 55 | // Valid platforms 56 | default: 57 | log.Fatalf("Unsupported platform: %s. Supported platforms: linux, darwin, windows", *platform) 58 | } 59 | 60 | // Validate architecture 61 | switch *arch { 62 | case "amd64", "arm64": 63 | // Valid architectures 64 | default: 65 | log.Fatalf("Unsupported architecture: %s. Supported architectures: amd64, arm64", *arch) 66 | } 67 | 68 | // Special case: Windows only supports amd64 69 | if *platform == "windows" && *arch != "amd64" { 70 | log.Fatalf("Windows platform only supports amd64 architecture") 71 | } 72 | 73 | fmt.Printf("Downloading CockroachDB binary for %s-%s", *platform, *arch) 74 | 75 | var actualVersion string 76 | var nonStable bool 77 | 78 | if *version == "unstable" { 79 | fmt.Printf(" (latest unstable)") 80 | actualVersion = "" 81 | nonStable = true 82 | } else { 83 | fmt.Printf(" version %s", *version) 84 | actualVersion = *version 85 | nonStable = false 86 | } 87 | fmt.Printf("\n") 88 | 89 | // Download the binary 90 | binaryPath, err := testserver.DownloadBinaryWithPlatform( 91 | &testserver.TestConfig{}, 92 | actualVersion, 93 | nonStable, 94 | *platform, 95 | *arch, 96 | *output, 97 | ) 98 | if err != nil { 99 | log.Fatalf("Failed to download binary: %v", err) 100 | } 101 | 102 | fmt.Printf("Successfully downloaded CockroachDB binary to: %s\n", binaryPath) 103 | } 104 | -------------------------------------------------------------------------------- /crdb/README.md: -------------------------------------------------------------------------------- 1 | CRDB 2 | ==== 3 | 4 | `crdb` is a wrapper around the logic for issuing SQL transactions which performs 5 | retries (as required by CockroachDB). 6 | 7 | ## Basic Usage 8 | 9 | ```go 10 | import "github.com/cockroachdb/cockroach-go/v2/crdb" 11 | 12 | err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 13 | // Your transaction logic here 14 | _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1") 15 | return err 16 | }) 17 | ``` 18 | 19 | ## Retry Policies 20 | 21 | By default, transactions retry up to 50 times with no delay between attempts. 22 | You can customize retry behavior using context options. 23 | 24 | ### Limiting Retries 25 | 26 | ```go 27 | // Retry up to 10 times 28 | ctx := crdb.WithMaxRetries(context.Background(), 10) 29 | err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 30 | // ... 31 | }) 32 | ``` 33 | 34 | ### Unlimited Retries 35 | 36 | ```go 37 | // Retry indefinitely (use with caution - ensure you have a context timeout!) 38 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 39 | defer cancel() 40 | ctx = crdb.WithMaxRetries(ctx, 0) 41 | ``` 42 | 43 | ### Disabling Retries 44 | 45 | ```go 46 | // Execute only once, no retries 47 | ctx := crdb.WithNoRetries(context.Background()) 48 | ``` 49 | 50 | ### Fixed Delay Between Retries 51 | 52 | ```go 53 | ctx := crdb.WithRetryPolicy(context.Background(), &crdb.LimitBackoffRetryPolicy{ 54 | RetryLimit: 10, 55 | Delay: 100 * time.Millisecond, 56 | }) 57 | ``` 58 | 59 | ### Exponential Backoff 60 | 61 | ```go 62 | ctx := crdb.WithRetryPolicy(context.Background(), &crdb.ExpBackoffRetryPolicy{ 63 | RetryLimit: 10, 64 | BaseDelay: 100 * time.Millisecond, // First retry waits 100ms 65 | MaxDelay: 5 * time.Second, // Cap delay at 5s 66 | }) 67 | // Delays: 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 5s, 5s, 5s, 5s 68 | ``` 69 | 70 | ### Custom Retry Policies 71 | 72 | Implement the `RetryPolicy` interface for custom behavior: 73 | 74 | ```go 75 | type RetryPolicy interface { 76 | NewRetry() RetryFunc 77 | } 78 | 79 | type RetryFunc func(err error) (delay time.Duration, retryErr error) 80 | ``` 81 | 82 | You can also adapt third-party backoff libraries using `ExternalBackoffPolicy()`: 83 | 84 | ```go 85 | import "github.com/sethvargo/go-retry" 86 | 87 | ctx := crdb.WithRetryPolicy(context.Background(), crdb.ExternalBackoffPolicy(func() crdb.ExternalBackoff { 88 | return retry.NewFibonacci(1 * time.Second) 89 | })) 90 | ``` 91 | 92 | ## Framework Support 93 | 94 | Subpackages provide support for popular frameworks: 95 | 96 | | Package | Framework | Import | 97 | |---------|-----------|--------| 98 | | `crdbpgx` | pgx v4 (standalone) | `github.com/cockroachdb/cockroach-go/v2/crdb/crdbpgx` | 99 | | `crdbpgxv5` | pgx v5 (standalone) | `github.com/cockroachdb/cockroach-go/v2/crdb/crdbpgxv5` | 100 | | `crdbgorm` | GORM | `github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm` | 101 | | `crdbsqlx` | sqlx | `github.com/cockroachdb/cockroach-go/v2/crdb/crdbsqlx` | 102 | 103 | ## Error Wrapping 104 | 105 | When wrapping errors inside transaction functions, use `%w` or `errors.Wrap()` 106 | to preserve retry detection: 107 | 108 | ```go 109 | // WRONG - masks retryable error 110 | return fmt.Errorf("failed: %s", err) 111 | 112 | // CORRECT - preserves error for retry detection 113 | return fmt.Errorf("failed: %w", err) 114 | ``` 115 | 116 | ## Driver Compatibility 117 | 118 | The library detects retryable errors using the `SQLState() string` method, 119 | which is implemented by: 120 | - [`github.com/lib/pq`](https://github.com/lib/pq) (v1.10.6+) 121 | - [`github.com/jackc/pgx`](https://github.com/jackc/pgx) (database/sql driver mode) 122 | 123 | ## Note for Developers 124 | 125 | If you make any changes here (especially if they modify public APIs), please 126 | verify that the code in https://github.com/cockroachdb/examples-go still works 127 | and update as necessary. 128 | -------------------------------------------------------------------------------- /crdb/crdbgorm/gorm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdbgorm 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/cockroachdb/cockroach-go/v2/crdb" 24 | "github.com/cockroachdb/cockroach-go/v2/testserver" 25 | "gorm.io/driver/postgres" 26 | "gorm.io/gorm" 27 | "gorm.io/gorm/logger" 28 | ) 29 | 30 | // TestExecuteTx verifies transaction retry using the classic example of write 31 | // skew in bank account balance transfers. 32 | func TestExecuteTx(t *testing.T) { 33 | ts, err := testserver.NewTestServer() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | ctx := context.Background() 38 | 39 | gormDB, err := gorm.Open(postgres.Open(ts.PGURL().String()), &gorm.Config{}) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | // Set to logger.Info and gorm logs all the queries. 44 | gormDB.Logger.LogMode(logger.Silent) 45 | 46 | if err := crdb.ExecuteTxGenericTest(ctx, gormWriteSkewTest{db: gormDB}); err != nil { 47 | t.Fatal(err) 48 | } 49 | } 50 | 51 | // TestExecuteTxFailsOnTransactionError verifies that the function throws an error if the 52 | // transaction fails to start 53 | func TestExecuteTxFailsOnTransactionError(t *testing.T) { 54 | ts, err := testserver.NewTestServer() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | ctx := context.Background() 59 | 60 | gormDB, err := gorm.Open(postgres.Open(ts.PGURL().String()), &gorm.Config{}) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | // Set to logger.Info and gorm logs all the queries. 65 | gormDB.Logger.LogMode(logger.Silent) 66 | 67 | // begin a transaction (bug) 68 | incorrectTx := gormDB.Begin() 69 | 70 | // attempt to execute a transaction - fails with no valid transaction 71 | err = ExecuteTx(ctx, incorrectTx, nil, func(tx *gorm.DB) error { 72 | return tx.Exec("SELECT 1+1").Error 73 | }) 74 | 75 | if !errors.Is(err, gorm.ErrInvalidTransaction) { 76 | t.Fail() 77 | } 78 | } 79 | 80 | // Account is our model, which corresponds to the "accounts" database 81 | // table. 82 | type Account struct { 83 | ID int `gorm:"primary_key"` 84 | Balance int 85 | } 86 | 87 | type gormWriteSkewTest struct { 88 | db *gorm.DB 89 | } 90 | 91 | var _ crdb.WriteSkewTest = gormWriteSkewTest{} 92 | 93 | func (t gormWriteSkewTest) Init(context.Context) error { 94 | t.db.AutoMigrate(&Account{}) 95 | t.db.Create(&Account{ID: 1, Balance: 100}) 96 | t.db.Create(&Account{ID: 2, Balance: 100}) 97 | return t.db.Error 98 | } 99 | 100 | // ExecuteTx is part of the crdb.WriteSkewTest interface. 101 | func (t gormWriteSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 102 | return ExecuteTx(ctx, t.db, nil /* opts */, func(tx *gorm.DB) error { 103 | return fn(tx) 104 | }) 105 | } 106 | 107 | // GetBalances is part of the crdb.WriteSkewTest interface. 108 | func (t gormWriteSkewTest) GetBalances(_ context.Context, txi interface{}) (int, int, error) { 109 | tx := txi.(*gorm.DB) 110 | 111 | var accounts []Account 112 | tx.Find(&accounts) 113 | if len(accounts) != 2 { 114 | return 0, 0, fmt.Errorf("expected two balances; got %d", len(accounts)) 115 | } 116 | return accounts[0].Balance, accounts[1].Balance, nil 117 | } 118 | 119 | // UpdateBalance is part of the crdb.WriteSkewInterface. 120 | func (t gormWriteSkewTest) UpdateBalance( 121 | _ context.Context, txi interface{}, accountID, delta int, 122 | ) error { 123 | tx := txi.(*gorm.DB) 124 | var acc Account 125 | tx.First(&acc, accountID) 126 | acc.Balance += delta 127 | return tx.Save(acc).Error 128 | } 129 | -------------------------------------------------------------------------------- /crdb/testing_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sync" 21 | ) 22 | 23 | // WriteSkewTest abstracts the operations that needs to be performed by a 24 | // particular framework for the purposes of TestExecuteTx. This allows the test 25 | // to be written once and run for any framework supported by this library. 26 | type WriteSkewTest interface { 27 | Init(context.Context) error 28 | ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error 29 | GetBalances(ctx context.Context, tx interface{}) (bal1, bal2 int, err error) 30 | UpdateBalance(ctx context.Context, tx interface{}, acct, delta int) error 31 | } 32 | 33 | // ExecuteTxGenericTest represents the structure of a test for the ExecuteTx 34 | // function. The actual database operations are abstracted by framework; the 35 | // idea is that tests for different frameworks implement that interface and then 36 | // invoke this test. 37 | // 38 | // The test interleaves two transactions such that one of them will require a 39 | // restart because of write skew. 40 | func ExecuteTxGenericTest(ctx context.Context, framework WriteSkewTest) error { 41 | framework.Init(ctx) 42 | // wg is used as a barrier, blocking each transaction after it performs the 43 | // initial read until they both read. 44 | var wg sync.WaitGroup 45 | wg.Add(2) 46 | runTxn := func(iter *int) <-chan error { 47 | errCh := make(chan error, 1) 48 | go func() { 49 | *iter = 0 50 | errCh <- framework.ExecuteTx(ctx, func(tx interface{}) (retErr error) { 51 | defer func() { 52 | if retErr == nil { 53 | return 54 | } 55 | // Wrap the error so that we test the library's unwrapping. 56 | retErr = testError{cause: retErr} 57 | }() 58 | 59 | *iter++ 60 | bal1, bal2, err := framework.GetBalances(ctx, tx) 61 | if err != nil { 62 | return err 63 | } 64 | // If this is the first iteration, wait for the other tx to also read. 65 | if *iter == 1 { 66 | wg.Done() 67 | wg.Wait() 68 | } 69 | // Now, subtract from one account and give to the other. 70 | if bal1 > bal2 { 71 | if err := framework.UpdateBalance(ctx, tx, 1, -100); err != nil { 72 | return err 73 | } 74 | if err := framework.UpdateBalance(ctx, tx, 2, +100); err != nil { 75 | return err 76 | } 77 | } else { 78 | if err := framework.UpdateBalance(ctx, tx, 1, +100); err != nil { 79 | return err 80 | } 81 | if err := framework.UpdateBalance(ctx, tx, 2, -100); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | }) 87 | }() 88 | return errCh 89 | } 90 | 91 | var iters1, iters2 int 92 | txn1Err := runTxn(&iters1) 93 | txn2Err := runTxn(&iters2) 94 | if err := <-txn1Err; err != nil { 95 | return fmt.Errorf("expected success in txn1; got %s", err) 96 | } 97 | if err := <-txn2Err; err != nil { 98 | return fmt.Errorf("expected success in txn2; got %s", err) 99 | } 100 | if iters1+iters2 <= 2 { 101 | return fmt.Errorf("expected at least one retry between the competing transactions; "+ 102 | "got txn1=%d, txn2=%d", iters1, iters2) 103 | } 104 | 105 | var bal1, bal2 int 106 | err := framework.ExecuteTx(ctx, func(txi interface{}) error { 107 | var err error 108 | bal1, bal2, err = framework.GetBalances(ctx, txi) 109 | return err 110 | }) 111 | if err != nil { 112 | return err 113 | } 114 | if bal1 != 100 || bal2 != 100 { 115 | return fmt.Errorf("expected balances to be restored without error; "+ 116 | "got acct1=%d, acct2=%d: %s", bal1, bal2, err) 117 | } 118 | return nil 119 | } 120 | 121 | type testError struct { 122 | cause error 123 | } 124 | 125 | func (t testError) Error() string { 126 | return "test error" 127 | } 128 | 129 | func (t testError) Unwrap() error { 130 | return t.cause 131 | } 132 | -------------------------------------------------------------------------------- /crdb/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import ( 18 | "context" 19 | "time" 20 | ) 21 | 22 | // Tx abstracts the operations needed by ExecuteInTx so that different 23 | // frameworks (e.g. go's sql package, pgx, gorm) can be used with ExecuteInTx. 24 | type Tx interface { 25 | Exec(context.Context, string, ...interface{}) error 26 | Commit(context.Context) error 27 | Rollback(context.Context) error 28 | } 29 | 30 | // ExecuteInTx runs fn inside tx. This method is primarily intended for internal 31 | // use. See other packages for higher-level, framework-specific ExecuteTx() 32 | // functions. 33 | // 34 | // *WARNING*: It is assumed that no statements have been executed on the 35 | // supplied Tx. ExecuteInTx will only retry statements that are performed within 36 | // the supplied closure (fn). Any statements performed on the tx before 37 | // ExecuteInTx is invoked will *not* be re-run if the transaction needs to be 38 | // retried. 39 | // 40 | // fn is subject to the same restrictions as the fn passed to ExecuteTx. 41 | func ExecuteInTx(ctx context.Context, tx Tx, fn func() error) (err error) { 42 | defer func() { 43 | r := recover() 44 | 45 | if r == nil && err == nil { 46 | // Ignore commit errors. The tx has already been committed by RELEASE. 47 | _ = tx.Commit(ctx) 48 | return 49 | } 50 | 51 | // We always need to execute a Rollback() so sql.DB releases the 52 | // connection. 53 | _ = tx.Rollback(ctx) 54 | 55 | if r != nil { 56 | panic(r) 57 | } 58 | }() 59 | 60 | // Specify that we intend to retry this txn in case of CockroachDB retryable 61 | // errors. 62 | if err = tx.Exec(ctx, "SAVEPOINT cockroach_restart"); err != nil { 63 | return err 64 | } 65 | 66 | // establish the retry policy 67 | retryPolicy := getRetryPolicy(ctx) 68 | // set up the retry policy state 69 | retryFunc := retryPolicy.NewRetry() 70 | for { 71 | releaseFailed := false 72 | err = fn() 73 | if err == nil { 74 | // RELEASE acts like COMMIT in CockroachDB. We use it since it gives us an 75 | // opportunity to react to retryable errors, whereas tx.Commit() doesn't. 76 | if err = tx.Exec(ctx, "RELEASE SAVEPOINT cockroach_restart"); err == nil { 77 | return nil 78 | } 79 | releaseFailed = true 80 | } 81 | 82 | // We got an error; let's see if it's a retryable one and, if so, restart. 83 | if !errIsRetryable(err) { 84 | if releaseFailed { 85 | err = newAmbiguousCommitError(err) 86 | } 87 | return err 88 | } 89 | 90 | // We have a retryable error. Check the retry policy. 91 | delay, retryErr := retryFunc(err) 92 | // Check if the context has been cancelled 93 | if ctxErr := ctx.Err(); ctxErr != nil { 94 | return ctxErr 95 | } 96 | if delay > 0 && retryErr == nil { 97 | // When backoff is needed, we don't want to hold locks while waiting for a backoff, 98 | // so restart the entire transaction: 99 | // - tx.Exec(ctx, "ROLLBACK") sends SQL to the server: 100 | // it doesn't call tx.Rollback() (which would close the Go sql.Tx object) 101 | // - The underlying connection remains open: the *sql.Tx wrapper maintains the database connection. 102 | // Only the server-side transaction is rolled back. 103 | // - tx.Exec(ctx, "BEGIN") starts a new server-side transaction on the same connection wrapped by the 104 | // same *sql.Tx object 105 | // - The defer handles cleanup - It calls tx.Rollback() (the Go method) only on errors, 106 | // which closes the Go object and returns the connection to the pool 107 | if restartErr := tx.Exec(ctx, "ROLLBACK"); restartErr != nil { 108 | return newTxnRestartError(restartErr, err, "ROLLBACK") 109 | } 110 | if restartErr := tx.Exec(ctx, "BEGIN"); restartErr != nil { 111 | return newTxnRestartError(restartErr, err, "BEGIN") 112 | } 113 | if restartErr := tx.Exec(ctx, "SAVEPOINT cockroach_restart"); restartErr != nil { 114 | return newTxnRestartError(restartErr, err, "SAVEPOINT cockroach_restart") 115 | } 116 | } else { 117 | if rollbackErr := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT cockroach_restart"); rollbackErr != nil { 118 | return newTxnRestartError(rollbackErr, err, "ROLLBACK TO SAVEPOINT cockroach_restart") 119 | } 120 | } 121 | 122 | if retryErr != nil { 123 | return retryErr 124 | } 125 | 126 | if delay > 0 { 127 | select { 128 | case <-time.After(delay): 129 | case <-ctx.Done(): 130 | return ctx.Err() 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /testserver/testservernode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package testserver 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "os" 21 | "os/exec" 22 | "strings" 23 | "syscall" 24 | ) 25 | 26 | func (ts *testServerImpl) StopNode(nodeNum int) error { 27 | ts.mu.Lock() 28 | ts.nodes[nodeNum].state = stateStopped 29 | ts.mu.Unlock() 30 | cmd := ts.nodes[nodeNum].startCmd 31 | 32 | // Kill the process. 33 | if cmd.Process != nil { 34 | if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { 35 | return err 36 | } 37 | if _, err := cmd.Process.Wait(); err != nil { 38 | return err 39 | } 40 | } 41 | // Reset the pgURL, since it could change if the node is started later; 42 | // specifically, if the listen port is 0 then the port will change. 43 | ts.pgURL[nodeNum] = pgURLChan{} 44 | ts.pgURL[nodeNum].started = make(chan struct{}) 45 | ts.pgURL[nodeNum].set = make(chan struct{}) 46 | 47 | if err := os.Remove(ts.nodes[nodeNum].listeningURLFile); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | type blockForeverReader struct{} 55 | 56 | func (blockForeverReader) Read(p []byte) (int, error) { 57 | select {} // block forever 58 | } 59 | 60 | func (ts *testServerImpl) StartNode(i int) error { 61 | ts.mu.RLock() 62 | if ts.nodes[i].state == stateRunning { 63 | return fmt.Errorf("node %d already running", i) 64 | } 65 | ts.mu.RUnlock() 66 | 67 | // We need to compute the join addresses here. since if the listen port is 68 | // 0, then the actual port will not be known until a node is started. 69 | var joinAddrs []string 70 | for otherNodeID := range ts.nodes { 71 | if i == otherNodeID { 72 | continue 73 | } 74 | if ts.serverArgs.listenAddrPorts[otherNodeID] != 0 { 75 | joinAddrs = append(joinAddrs, fmt.Sprintf("localhost:%d", ts.serverArgs.listenAddrPorts[otherNodeID])) 76 | continue 77 | } 78 | select { 79 | case <-ts.pgURL[otherNodeID].started: 80 | // PGURLForNode will block until the URL is ready. If something 81 | // goes wrong, the goroutine waiting on pollListeningURLFile 82 | // will time out. 83 | joinAddrs = append(joinAddrs, fmt.Sprintf("localhost:%s", ts.PGURLForNode(otherNodeID).Port())) 84 | default: 85 | // If the other node hasn't started yet, don't add the join arg. 86 | } 87 | } 88 | joinArg := fmt.Sprintf("--join=%s", strings.Join(joinAddrs, ",")) 89 | 90 | args := ts.nodes[i].startCmdArgs 91 | if len(ts.nodes) > 1 { 92 | if len(joinAddrs) == 0 { 93 | // The start command always requires a --join arg, so we fake one 94 | // if we don't have any yet. 95 | joinArg = "--join=localhost:0" 96 | } 97 | args = append(args, joinArg) 98 | } 99 | ts.nodes[i].startCmd = exec.Command(args[0], args[1:]...) 100 | 101 | currCmd := ts.nodes[i].startCmd 102 | currCmd.Env = []string{ 103 | "COCKROACH_MAX_OFFSET=1ns", 104 | "COCKROACH_TRUST_CLIENT_PROVIDED_SQL_REMOTE_ADDR=true", 105 | } 106 | 107 | // Set the working directory of the cockroach process to our temp folder. 108 | // This stops cockroach from polluting the project directory with _dump 109 | // folders. 110 | currCmd.Dir = ts.baseDir 111 | 112 | if len(ts.nodes[i].stdout) > 0 { 113 | wr, err := newFileLogWriter(ts.nodes[i].stdout) 114 | if err != nil { 115 | return fmt.Errorf("unable to open file %s: %w", ts.nodes[i].stdout, err) 116 | } 117 | ts.nodes[i].stdoutBuf = wr 118 | } 119 | currCmd.Stdout = ts.nodes[i].stdoutBuf 120 | 121 | if len(ts.nodes[i].stderr) > 0 { 122 | wr, err := newFileLogWriter(ts.nodes[i].stderr) 123 | if err != nil { 124 | return fmt.Errorf("unable to open file %s: %w", ts.nodes[1].stderr, err) 125 | } 126 | ts.nodes[i].stderrBuf = wr 127 | } 128 | currCmd.Stderr = ts.nodes[i].stderrBuf 129 | 130 | if ts.serverArgs.demoMode { 131 | currCmd.Stdin = blockForeverReader{} 132 | } 133 | 134 | for k, v := range defaultEnv() { 135 | currCmd.Env = append(currCmd.Env, k+"="+v) 136 | } 137 | // Allow caller-provided environment variables to override defaults. 138 | currCmd.Env = append(currCmd.Env, ts.serverArgs.envVars...) 139 | // For demo clusters, force HOME into a sandbox-writable directory under 140 | // the testserver temp dir so that demo's ~/.cockroach-demo sockets work 141 | // in sandboxed environments. 142 | if ts.serverArgs.demoMode { 143 | // Prefer a very short path to stay under Unix-domain socket length limits. 144 | demoHome, err := os.MkdirTemp("/tmp", "cockroach-demo-*") 145 | if err != nil { 146 | return fmt.Errorf("unable to create demo HOME directory %s: %w", demoHome, err) 147 | } 148 | currCmd.Env = append(currCmd.Env, "HOME="+demoHome) 149 | } 150 | 151 | log.Printf("executing: %s", currCmd) 152 | err := currCmd.Start() 153 | close(ts.pgURL[i].started) 154 | if currCmd.Process != nil { 155 | log.Printf("process %d started. env=%s; cmd: %s", currCmd.Process.Pid, currCmd.Env, strings.Join(args, " ")) 156 | } 157 | if err != nil { 158 | log.Print(err.Error()) 159 | ts.mu.Lock() 160 | ts.nodes[i].state = stateFailed 161 | ts.mu.Unlock() 162 | 163 | return fmt.Errorf("command %s failed: %w", currCmd, err) 164 | } 165 | 166 | ts.mu.Lock() 167 | ts.nodes[i].state = stateRunning 168 | ts.mu.Unlock() 169 | 170 | capturedI := i 171 | 172 | if ts.pgURL[capturedI].u == nil { 173 | if err := ts.pollListeningURLFile(capturedI); err != nil { 174 | log.Printf("%s failed to poll listening URL file: %v", testserverMessagePrefix, err) 175 | close(ts.pgURL[capturedI].set) 176 | ts.Stop() 177 | return err 178 | } 179 | } 180 | 181 | return nil 182 | } 183 | 184 | func (ts *testServerImpl) UpgradeNode(nodeNum int) error { 185 | err := ts.StopNode(nodeNum) 186 | if err != nil { 187 | return err 188 | } 189 | ts.nodes[nodeNum].startCmdArgs[0] = ts.serverArgs.upgradeCockroachBinary 190 | return ts.StartNode(nodeNum) 191 | } 192 | -------------------------------------------------------------------------------- /crdb/tx_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import ( 18 | "context" 19 | "database/sql" 20 | "errors" 21 | "fmt" 22 | "testing" 23 | 24 | "github.com/cockroachdb/cockroach-go/v2/testserver" 25 | ) 26 | 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 | 89 | // TestExecuteTx verifies transaction retry using the classic 90 | // example of write skew in bank account balance transfers. 91 | func TestExecuteTx(t *testing.T) { 92 | db, stop := testserver.NewDBForTest(t) 93 | defer stop() 94 | ctx := context.Background() 95 | 96 | if err := ExecuteTxGenericTest(ctx, stdlibWriteSkewTest{db: db}); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | 101 | // TestConfigureRetries verifies that the number of retries can be specified 102 | // via context. 103 | func TestConfigureRetries(t *testing.T) { 104 | // Test no retries (using WithNoRetries) 105 | ctx := WithNoRetries(context.Background()) 106 | requireRetries(t, ctx, 0) 107 | 108 | // Test single retry 109 | ctx = WithMaxRetries(context.Background(), 1) 110 | requireRetries(t, ctx, 1) 111 | 112 | // Test default retries 113 | ctx = context.Background() 114 | requireRetries(t, ctx, defaultRetries) 115 | 116 | // Test custom retry limit 117 | ctx = WithMaxRetries(context.Background(), 123+defaultRetries) 118 | requireRetries(t, ctx, 123+defaultRetries) 119 | 120 | // Test exponential backoff policy 121 | ctx = WithRetryPolicy(context.Background(), &ExpBackoffRetryPolicy{ 122 | RetryLimit: 10, 123 | BaseDelay: 10, 124 | MaxDelay: 1000, 125 | }) 126 | requireRetries(t, ctx, 10) 127 | 128 | // Test unlimited retries (0) - can't test easily without infinite loop, 129 | // so we just verify the policy is set correctly 130 | ctx = WithMaxRetries(context.Background(), 0) 131 | p := getRetryPolicy(ctx) 132 | if lbp, ok := p.(*LimitBackoffRetryPolicy); ok { 133 | if lbp.RetryLimit != UnlimitedRetries { 134 | t.Fatalf("expected UnlimitedRetries (0), got %d", lbp.RetryLimit) 135 | } 136 | } else { 137 | t.Fatal("expected LimitBackoffRetryPolicy") 138 | } 139 | } 140 | 141 | func requireRetries(t *testing.T, ctx context.Context, numRetries int) { 142 | p := getRetryPolicy(ctx) 143 | if p == nil { 144 | t.Fatal("expected non-nil retry policy") 145 | } 146 | 147 | rf := p.NewRetry() 148 | tryCount := 0 149 | for { 150 | // we try 151 | tryCount++ 152 | 153 | // Then, decide whether we're out of retries. 154 | // The first try is not a retry, so we should 155 | _, err := rf(nil) 156 | if err != nil { 157 | retryCount := tryCount - 1 158 | if retryCount != numRetries { 159 | t.Fatalf("expected %d retries, got %d", numRetries, retryCount) 160 | } 161 | return 162 | } 163 | } 164 | } 165 | 166 | type stdlibWriteSkewTest struct { 167 | db *sql.DB 168 | } 169 | 170 | var _ WriteSkewTest = stdlibWriteSkewTest{} 171 | 172 | func (t stdlibWriteSkewTest) Init(ctx context.Context) error { 173 | initStmt := ` 174 | CREATE DATABASE d; 175 | CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT); 176 | INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); 177 | ` 178 | _, err := t.db.ExecContext(ctx, initStmt) 179 | return err 180 | } 181 | 182 | func (t stdlibWriteSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 183 | return ExecuteTx(ctx, t.db, nil /* opts */, func(tx *sql.Tx) error { 184 | return fn(tx) 185 | }) 186 | } 187 | 188 | func (t stdlibWriteSkewTest) GetBalances(ctx context.Context, txi interface{}) (int, int, error) { 189 | tx := txi.(*sql.Tx) 190 | var rows *sql.Rows 191 | rows, err := tx.QueryContext(ctx, `SELECT balance FROM d.t WHERE acct IN (1, 2);`) 192 | if err != nil { 193 | return 0, 0, err 194 | } 195 | defer rows.Close() 196 | var bal1, bal2 int 197 | balances := []*int{&bal1, &bal2} 198 | i := 0 199 | for ; rows.Next(); i++ { 200 | if err = rows.Scan(balances[i]); err != nil { 201 | return 0, 0, err 202 | } 203 | } 204 | if i != 2 { 205 | return 0, 0, fmt.Errorf("expected two balances; got %d", i) 206 | } 207 | return bal1, bal2, nil 208 | } 209 | 210 | func (t stdlibWriteSkewTest) UpdateBalance( 211 | ctx context.Context, txi interface{}, acct, delta int, 212 | ) error { 213 | tx := txi.(*sql.Tx) 214 | _, err := tx.ExecContext(ctx, `UPDATE d.t SET balance=balance+$1 WHERE acct=$2;`, delta, acct) 215 | return err 216 | } 217 | -------------------------------------------------------------------------------- /testserver/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "regexp" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // Version represents a semantic version; see 26 | // https://semver.org/spec/v2.0.0.html. 27 | type Version struct { 28 | major int32 29 | minor int32 30 | patch int32 31 | preRelease string 32 | metadata string 33 | } 34 | 35 | // Major returns the major (first) version number. 36 | func (v *Version) Major() int { 37 | return int(v.major) 38 | } 39 | 40 | // Minor returns the minor (second) version number. 41 | func (v *Version) Minor() int { 42 | return int(v.minor) 43 | } 44 | 45 | // Patch returns the patch (third) version number. 46 | func (v *Version) Patch() int { 47 | return int(v.patch) 48 | } 49 | 50 | // PreRelease returns the pre-release version (if present). 51 | func (v *Version) PreRelease() string { 52 | return v.preRelease 53 | } 54 | 55 | // Metadata returns the metadata (if present). 56 | func (v *Version) Metadata() string { 57 | return v.metadata 58 | } 59 | 60 | // String returns the string representation, in the format: 61 | // 62 | // "v1.2.3-beta+md" 63 | func (v Version) String() string { 64 | var b bytes.Buffer 65 | fmt.Fprintf(&b, "v%d.%d.%d", v.major, v.minor, v.patch) 66 | if v.preRelease != "" { 67 | fmt.Fprintf(&b, "-%s", v.preRelease) 68 | } 69 | if v.metadata != "" { 70 | fmt.Fprintf(&b, "+%s", v.metadata) 71 | } 72 | return b.String() 73 | } 74 | 75 | // versionRE is the regexp that is used to verify that a version string is 76 | // of the form "vMAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]". This 77 | // conforms to https://semver.org/spec/v2.0.0.html 78 | var versionRE = regexp.MustCompile( 79 | `^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+|)?$`, 80 | // ^major ^minor ^patch ^preRelease ^metadata 81 | ) 82 | 83 | // numericRE is the regexp used to check if an identifier is numeric. 84 | var numericRE = regexp.MustCompile(`^(0|[1-9][0-9]*)$`) 85 | 86 | // Parse creates a version from a string. The string must be a valid semantic 87 | // version (as per https://semver.org/spec/v2.0.0.html) in the format: 88 | // 89 | // "vMINOR.MAJOR.PATCH[-PRERELEASE][+METADATA]". 90 | // 91 | // MINOR, MAJOR, and PATCH are numeric values (without any leading 0s). 92 | // PRERELEASE and METADATA can contain ASCII characters and digits, hyphens and 93 | // dots. 94 | func Parse(str string) (*Version, error) { 95 | if !versionRE.MatchString(str) { 96 | return nil, fmt.Errorf("invalid version string '%s'", str) 97 | } 98 | 99 | var v Version 100 | r := strings.NewReader(str) 101 | // Read the major.minor.patch part. 102 | _, err := fmt.Fscanf(r, "v%d.%d.%d", &v.major, &v.minor, &v.patch) 103 | if err != nil { 104 | panic(fmt.Sprintf("invalid version '%s' passed the regex: %s", str, err)) 105 | } 106 | remaining := str[len(str)-r.Len():] 107 | // Read the pre-release, if present. 108 | if len(remaining) > 0 && remaining[0] == '-' { 109 | p := strings.IndexRune(remaining, '+') 110 | if p == -1 { 111 | p = len(remaining) 112 | } 113 | v.preRelease = remaining[1:p] 114 | remaining = remaining[p:] 115 | } 116 | // Read the metadata, if present. 117 | if len(remaining) > 0 { 118 | if remaining[0] != '+' { 119 | panic(fmt.Sprintf("invalid version '%s' passed the regex", str)) 120 | } 121 | v.metadata = remaining[1:] 122 | } 123 | return &v, nil 124 | } 125 | 126 | // MustParse is like Parse but panics on any error. Recommended as an 127 | // initializer for global values. 128 | func MustParse(str string) *Version { 129 | v, err := Parse(str) 130 | if err != nil { 131 | panic(err) 132 | } 133 | return v 134 | } 135 | 136 | func cmpVal(a, b int32) int { 137 | if a > b { 138 | return +1 139 | } 140 | if a < b { 141 | return -1 142 | } 143 | return 0 144 | } 145 | 146 | // Compare returns -1, 0, or +1 indicating the relative ordering of versions. 147 | func (v *Version) Compare(w *Version) int { 148 | if v := cmpVal(v.major, w.major); v != 0 { 149 | return v 150 | } 151 | if v := cmpVal(v.minor, w.minor); v != 0 { 152 | return v 153 | } 154 | if v := cmpVal(v.patch, w.patch); v != 0 { 155 | return v 156 | } 157 | if v.preRelease != w.preRelease { 158 | if v.preRelease == "" && w.preRelease != "" { 159 | // 1.0.0 is greater than 1.0.0-alpha. 160 | return 1 161 | } 162 | if v.preRelease != "" && w.preRelease == "" { 163 | // 1.0.0-alpha is less than 1.0.0. 164 | return -1 165 | } 166 | 167 | // Quoting from https://semver.org/spec/v2.0.0.html: 168 | // Precedence for two pre-release versions with the same major, minor, and 169 | // patch version MUST be determined by comparing each dot separated 170 | // identifier from left to right until a difference is found as follows: 171 | // (1) Identifiers consisting of only digits are compared numerically. 172 | // (2) identifiers with letters or hyphens are compared lexically in ASCII 173 | // sort order. 174 | // (3) Numeric identifiers always have lower precedence than non-numeric 175 | // identifiers. 176 | // (4) A larger set of pre-release fields has a higher precedence than a 177 | // smaller set, if all of the preceding identifiers are equal. 178 | // 179 | vs := strings.Split(v.preRelease, ".") 180 | ws := strings.Split(w.preRelease, ".") 181 | for ; len(vs) > 0 && len(ws) > 0; vs, ws = vs[1:], ws[1:] { 182 | vStr, wStr := vs[0], ws[0] 183 | if vStr == wStr { 184 | continue 185 | } 186 | vNumeric := numericRE.MatchString(vStr) 187 | wNumeric := numericRE.MatchString(wStr) 188 | switch { 189 | case vNumeric && wNumeric: 190 | // Case 1. 191 | vVal, err := strconv.Atoi(vStr) 192 | if err != nil { 193 | panic(err) 194 | } 195 | wVal, err := strconv.Atoi(wStr) 196 | if err != nil { 197 | panic(err) 198 | } 199 | if vVal == wVal { 200 | panic("different strings yield the same numbers") 201 | } 202 | if vVal < wVal { 203 | return -1 204 | } 205 | return 1 206 | 207 | case vNumeric: 208 | // Case 3. 209 | return -1 210 | 211 | case wNumeric: 212 | // Case 3. 213 | return 1 214 | 215 | default: 216 | // Case 2. 217 | if vStr < wStr { 218 | return -1 219 | } 220 | return 1 221 | } 222 | } 223 | 224 | if len(vs) > 0 { 225 | // Case 4. 226 | return +1 227 | } 228 | if len(ws) > 0 { 229 | // Case 4. 230 | return -1 231 | } 232 | } 233 | 234 | return 0 235 | } 236 | 237 | // AtLeast returns true if v >= w. 238 | func (v *Version) AtLeast(w *Version) bool { 239 | return v.Compare(w) >= 0 240 | } 241 | -------------------------------------------------------------------------------- /crdb/retry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func assertDelays(t *testing.T, policy RetryPolicy, expectedDelays []time.Duration) { 24 | t.Helper() 25 | actualDelays := make([]time.Duration, 0, len(expectedDelays)) 26 | rf := policy.NewRetry() 27 | 28 | // Test with nil error (normal retry case) 29 | for { 30 | delay, err := rf(nil) 31 | if err != nil { 32 | break 33 | } 34 | 35 | actualDelays = append(actualDelays, delay) 36 | if len(actualDelays) > len(expectedDelays) { 37 | t.Fatalf("too many retries: expected %d", len(expectedDelays)) 38 | } 39 | } 40 | if len(actualDelays) != len(expectedDelays) { 41 | t.Errorf("wrong number of retries: expected %d, got %d", len(expectedDelays), len(actualDelays)) 42 | } 43 | for i, delay := range actualDelays { 44 | expected := expectedDelays[i] 45 | if delay != expected { 46 | t.Errorf("wrong delay at index %d: expected %d, got %d", i, expected, delay) 47 | } 48 | } 49 | 50 | // Test that RetryFunc also works when passed a non-nil error 51 | // The error passed to RetryFunc should not affect the retry logic 52 | rf2 := policy.NewRetry() 53 | testErr := errors.New("test retryable error") 54 | actualDelays2 := make([]time.Duration, 0, len(expectedDelays)) 55 | for { 56 | delay, err := rf2(testErr) 57 | if err != nil { 58 | break 59 | } 60 | actualDelays2 = append(actualDelays2, delay) 61 | if len(actualDelays2) > len(expectedDelays) { 62 | t.Fatalf("too many retries with non-nil err: expected %d", len(expectedDelays)) 63 | } 64 | } 65 | if len(actualDelays2) != len(expectedDelays) { 66 | t.Errorf("wrong number of retries with non-nil err: expected %d, got %d", len(expectedDelays), len(actualDelays2)) 67 | } 68 | } 69 | 70 | func TestLimitBackoffRetryPolicy(t *testing.T) { 71 | policy := &LimitBackoffRetryPolicy{ 72 | RetryLimit: 3, 73 | Delay: 1 * time.Second, 74 | } 75 | assertDelays(t, policy, []time.Duration{ 76 | 1 * time.Second, 77 | 1 * time.Second, 78 | 1 * time.Second, 79 | }) 80 | } 81 | 82 | func TestExpBackoffRetryPolicy(t *testing.T) { 83 | policy := &ExpBackoffRetryPolicy{ 84 | RetryLimit: 5, 85 | BaseDelay: 1 * time.Second, 86 | MaxDelay: 5 * time.Second, 87 | } 88 | assertDelays(t, policy, []time.Duration{ 89 | 1 * time.Second, 90 | 2 * time.Second, 91 | 4 * time.Second, 92 | 5 * time.Second, 93 | 5 * time.Second, 94 | }) 95 | } 96 | 97 | func TestNoRetries(t *testing.T) { 98 | policy := &LimitBackoffRetryPolicy{ 99 | RetryLimit: NoRetries, 100 | Delay: 0, 101 | } 102 | // NoRetries should fail immediately without any retries 103 | assertDelays(t, policy, []time.Duration{}) 104 | 105 | // Verify the error is returned on first call 106 | rf := policy.NewRetry() 107 | testErr := errors.New("test error") 108 | delay, err := rf(testErr) 109 | if err == nil { 110 | t.Error("expected error on first call with NoRetries, got nil") 111 | } 112 | if delay != 0 { 113 | t.Errorf("expected delay 0, got %v", delay) 114 | } 115 | } 116 | 117 | func TestUnlimitedRetries(t *testing.T) { 118 | policy := &LimitBackoffRetryPolicy{ 119 | RetryLimit: UnlimitedRetries, 120 | Delay: 10 * time.Millisecond, 121 | } 122 | 123 | // Test that UnlimitedRetries continues beyond any reasonable limit 124 | rf := policy.NewRetry() 125 | testErr := errors.New("test error") 126 | 127 | // Try 1000 retries - should all succeed with no error 128 | for i := 0; i < 1000; i++ { 129 | delay, err := rf(testErr) 130 | if err != nil { 131 | t.Fatalf("unexpected error at retry %d: %v", i, err) 132 | } 133 | if delay != 10*time.Millisecond { 134 | t.Errorf("wrong delay at retry %d: expected 10ms, got %v", i, delay) 135 | } 136 | } 137 | } 138 | 139 | func TestLimitBackoffRetryPolicyEdgeCases(t *testing.T) { 140 | t.Run("zero BaseDelay with LimitBackoffRetryPolicy", func(t *testing.T) { 141 | policy := &LimitBackoffRetryPolicy{ 142 | RetryLimit: 3, 143 | Delay: 0, // zero delay = immediate retries 144 | } 145 | assertDelays(t, policy, []time.Duration{0, 0, 0}) 146 | }) 147 | 148 | t.Run("negative RetryLimit less than NoRetries", func(t *testing.T) { 149 | // Negative values other than NoRetries (-1) should be treated as invalid 150 | // but the implementation currently treats any negative as "no retries" 151 | policy := &LimitBackoffRetryPolicy{ 152 | RetryLimit: -5, 153 | Delay: 0, 154 | } 155 | rf := policy.NewRetry() 156 | _, err := rf(errors.New("test")) 157 | // Should fail immediately like NoRetries 158 | if err == nil { 159 | t.Error("expected error for negative RetryLimit < NoRetries, got nil") 160 | } 161 | }) 162 | 163 | t.Run("very large RetryLimit", func(t *testing.T) { 164 | policy := &LimitBackoffRetryPolicy{ 165 | RetryLimit: 1000000, 166 | Delay: 0, 167 | } 168 | rf := policy.NewRetry() 169 | // Should be able to retry many times 170 | for i := 0; i < 100; i++ { 171 | _, err := rf(nil) 172 | if err != nil { 173 | t.Fatalf("unexpected error at retry %d with large limit: %v", i, err) 174 | } 175 | } 176 | }) 177 | } 178 | 179 | func TestExpBackoffRetryPolicyEdgeCases(t *testing.T) { 180 | t.Run("zero BaseDelay", func(t *testing.T) { 181 | policy := &ExpBackoffRetryPolicy{ 182 | RetryLimit: 3, 183 | BaseDelay: 0, 184 | MaxDelay: 1 * time.Second, 185 | } 186 | // With zero base delay, all delays should be 0 187 | assertDelays(t, policy, []time.Duration{0, 0, 0}) 188 | }) 189 | 190 | t.Run("MaxDelay less than BaseDelay", func(t *testing.T) { 191 | policy := &ExpBackoffRetryPolicy{ 192 | RetryLimit: 3, 193 | BaseDelay: 1 * time.Second, 194 | MaxDelay: 100 * time.Millisecond, // smaller than base 195 | } 196 | // All delays should be capped at MaxDelay 197 | assertDelays(t, policy, []time.Duration{ 198 | 100 * time.Millisecond, 199 | 100 * time.Millisecond, 200 | 100 * time.Millisecond, 201 | }) 202 | }) 203 | 204 | t.Run("MaxDelay equals BaseDelay", func(t *testing.T) { 205 | policy := &ExpBackoffRetryPolicy{ 206 | RetryLimit: 3, 207 | BaseDelay: 1 * time.Second, 208 | MaxDelay: 1 * time.Second, // same as base 209 | } 210 | // All delays should be capped at MaxDelay (no exponential growth) 211 | assertDelays(t, policy, []time.Duration{ 212 | 1 * time.Second, 213 | 1 * time.Second, 214 | 1 * time.Second, 215 | }) 216 | }) 217 | 218 | t.Run("zero MaxDelay with potential overflow", func(t *testing.T) { 219 | policy := &ExpBackoffRetryPolicy{ 220 | RetryLimit: 100, 221 | BaseDelay: 1 * time.Hour, 222 | MaxDelay: 0, // no cap 223 | } 224 | rf := policy.NewRetry() 225 | 226 | // First few should work fine 227 | for i := 0; i < 5; i++ { 228 | delay, err := rf(nil) 229 | if err != nil { 230 | t.Fatalf("unexpected error at retry %d: %v", i, err) 231 | } 232 | expected := (1 * time.Hour) << i 233 | if delay != expected { 234 | t.Errorf("retry %d: expected delay %v, got %v", i, expected, delay) 235 | } 236 | } 237 | 238 | // Eventually should overflow and fail 239 | var overflowed bool 240 | for i := 5; i < 100; i++ { 241 | _, err := rf(nil) 242 | if err != nil { 243 | overflowed = true 244 | break 245 | } 246 | } 247 | if !overflowed { 248 | t.Error("expected overflow error with large base delay and no MaxDelay") 249 | } 250 | }) 251 | 252 | t.Run("single retry with exponential backoff", func(t *testing.T) { 253 | policy := &ExpBackoffRetryPolicy{ 254 | RetryLimit: 1, 255 | BaseDelay: 100 * time.Millisecond, 256 | MaxDelay: 0, 257 | } 258 | assertDelays(t, policy, []time.Duration{100 * time.Millisecond}) 259 | }) 260 | 261 | t.Run("NoRetries with ExpBackoffRetryPolicy", func(t *testing.T) { 262 | policy := &ExpBackoffRetryPolicy{ 263 | RetryLimit: NoRetries, 264 | BaseDelay: 1 * time.Second, 265 | MaxDelay: 5 * time.Second, 266 | } 267 | // NoRetries should fail immediately without any retries 268 | assertDelays(t, policy, []time.Duration{}) 269 | 270 | // Verify the error is returned on first call 271 | rf := policy.NewRetry() 272 | testErr := errors.New("test error") 273 | delay, err := rf(testErr) 274 | if err == nil { 275 | t.Error("expected error on first call with NoRetries, got nil") 276 | } 277 | if delay != 0 { 278 | t.Errorf("expected delay 0, got %v", delay) 279 | } 280 | }) 281 | 282 | t.Run("UnlimitedRetries with ExpBackoffRetryPolicy", func(t *testing.T) { 283 | policy := &ExpBackoffRetryPolicy{ 284 | RetryLimit: UnlimitedRetries, 285 | BaseDelay: 10 * time.Millisecond, 286 | MaxDelay: 100 * time.Millisecond, 287 | } 288 | 289 | // Test that UnlimitedRetries continues beyond any reasonable limit 290 | rf := policy.NewRetry() 291 | testErr := errors.New("test error") 292 | 293 | // Try 100 retries - should all succeed with no error 294 | // Delays should follow exponential backoff until capped at MaxDelay 295 | for i := 0; i < 100; i++ { 296 | delay, err := rf(testErr) 297 | if err != nil { 298 | t.Fatalf("unexpected error at retry %d: %v", i, err) 299 | } 300 | // After a few retries, delay should be capped at MaxDelay 301 | if i < 4 { 302 | // For the first few retries, check exact exponential values 303 | expectedDelay := (10 * time.Millisecond) << i 304 | if delay != expectedDelay { 305 | t.Errorf("wrong delay at retry %d: expected %v, got %v", i, expectedDelay, delay) 306 | } 307 | } else { 308 | // After that, should be capped at MaxDelay 309 | if delay != 100*time.Millisecond { 310 | t.Errorf("wrong delay at retry %d: expected 100ms (capped), got %v", i, delay) 311 | } 312 | } 313 | } 314 | }) 315 | 316 | t.Run("negative RetryLimit with ExpBackoffRetryPolicy", func(t *testing.T) { 317 | policy := &ExpBackoffRetryPolicy{ 318 | RetryLimit: -5, 319 | BaseDelay: 1 * time.Second, 320 | MaxDelay: 5 * time.Second, 321 | } 322 | rf := policy.NewRetry() 323 | _, err := rf(errors.New("test")) 324 | // Should fail immediately like NoRetries 325 | if err == nil { 326 | t.Error("expected error for negative RetryLimit, got nil") 327 | } 328 | }) 329 | } 330 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /testserver/tenant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package testserver 16 | 17 | import ( 18 | "bytes" 19 | "database/sql" 20 | "errors" 21 | "fmt" 22 | "log" 23 | "net" 24 | "net/url" 25 | "os/exec" 26 | "path/filepath" 27 | "strconv" 28 | "strings" 29 | 30 | "github.com/cockroachdb/cockroach-go/v2/testserver/version" 31 | ) 32 | 33 | func (ts *testServerImpl) isTenant() bool { 34 | // ts.curTenantID is initialized to firstTenantID in system tenant servers. 35 | // An uninitialized ts.curTenantID indicates that this TestServer is a 36 | // tenant. 37 | return ts.curTenantID < firstTenantID 38 | } 39 | 40 | // cockroachSupportsTenantScopeCert is a hack to figure out if the version of 41 | // cockroach on the test server supports tenant scoped certificates. This is less 42 | // brittle than a static version comparison as these tenant scoped certificates are 43 | // subject to backports to older CRDB verions. 44 | func (ts *testServerImpl) cockroachSupportsTenantScopeCert() (bool, error) { 45 | certCmdArgs := []string{ 46 | "cert", 47 | "create-client", 48 | "--help", 49 | } 50 | checkTenantScopeCertCmd := exec.Command(ts.serverArgs.cockroachBinary, certCmdArgs...) 51 | var output bytes.Buffer 52 | checkTenantScopeCertCmd.Stdout = &output 53 | if err := checkTenantScopeCertCmd.Run(); err != nil { 54 | return false, err 55 | } 56 | return strings.Contains(output.String(), "--tenant-scope"), nil 57 | } 58 | 59 | // NewTenantServer creates and returns a new SQL tenant pointed at the receiver, 60 | // which acts as a KV server, and starts it. 61 | // The SQL tenant is responsible for all SQL processing and does not store any 62 | // physical KV pairs. It issues KV RPCs to the receiver. The idea is to be able 63 | // to create multiple SQL tenants each with an exclusive keyspace accessed 64 | // through the KV server. The proxy bool determines whether to spin up a 65 | // (singleton) proxy instance to which to direct the returned server's `PGUrl` 66 | // method. 67 | // 68 | // WARNING: This functionality is internal and experimental and subject to 69 | // change. See cockroach mt start-sql --help. 70 | // NOTE: To use this, a caller must first define an interface that includes 71 | // NewTenantServer, and subsequently cast the TestServer obtained from 72 | // NewTestServer to this interface. Refer to the tests for an example. 73 | func (ts *testServerImpl) NewTenantServer(proxy bool) (TestServer, error) { 74 | if proxy && !ts.serverArgs.secure { 75 | return nil, fmt.Errorf("%s: proxy cannot be used with insecure mode", tenantserverMessagePrefix) 76 | } 77 | cockroachBinary := ts.serverArgs.cockroachBinary 78 | tenantID, err := func() (int, error) { 79 | ts.mu.Lock() 80 | defer ts.mu.Unlock() 81 | if ts.nodes[0].state != stateRunning { 82 | return 0, errors.New("TestServer must be running before NewTenantServer may be called") 83 | } 84 | if ts.isTenant() { 85 | return 0, errors.New("cannot call NewTenantServer on a tenant") 86 | } 87 | tenantID := ts.curTenantID 88 | ts.curTenantID++ 89 | return tenantID, nil 90 | }() 91 | if err != nil { 92 | return nil, fmt.Errorf("%s: %w", tenantserverMessagePrefix, err) 93 | } 94 | 95 | secureFlag := "--insecure" 96 | certsDir := filepath.Join(ts.baseDir, "certs") 97 | if ts.serverArgs.secure { 98 | secureFlag = "--certs-dir=" + certsDir 99 | // Create tenant client certificate. 100 | certArgs := []string{"mt", "cert", "create-tenant-client", fmt.Sprint(tenantID)} 101 | if ts.version.AtLeast(version.MustParse("v22.1.0-alpha")) { 102 | certArgs = append(certArgs, "127.0.0.1", "[::1]", "localhost", "*.local") 103 | } 104 | certArgs = append(certArgs, secureFlag, "--ca-key="+filepath.Join(certsDir, "ca.key")) 105 | createCertCmd := exec.Command(cockroachBinary, certArgs...) 106 | log.Printf("%s executing: %s", tenantserverMessagePrefix, createCertCmd) 107 | if err := createCertCmd.Run(); err != nil { 108 | return nil, fmt.Errorf("%s command %s failed: %w", tenantserverMessagePrefix, createCertCmd, err) 109 | } 110 | tenantScopeCertsAvailable, err := ts.cockroachSupportsTenantScopeCert() 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to determine if tenant scoped certificates are available: %w", err) 113 | } 114 | if tenantScopeCertsAvailable { 115 | // Overwrite root client certificate scoped to the system and current tenant. 116 | // Tenant scoping is needed for client certificates used to access tenant servers. 117 | tenantScopedClientCertArgs := []string{ 118 | "cert", 119 | "create-client", 120 | "root", 121 | "--also-generate-pkcs8-key", 122 | fmt.Sprintf("--tenant-scope=1,%d", tenantID), 123 | secureFlag, 124 | "--ca-key=" + filepath.Join(certsDir, "ca.key"), 125 | "--overwrite", 126 | } 127 | clientCertCmd := exec.Command(cockroachBinary, tenantScopedClientCertArgs...) 128 | log.Printf("%s overwriting root client cert with tenant scoped root client cert: %s", tenantserverMessagePrefix, clientCertCmd) 129 | if err := clientCertCmd.Run(); err != nil { 130 | return nil, fmt.Errorf("%s command %s failed: %w", tenantserverMessagePrefix, clientCertCmd, err) 131 | } 132 | } 133 | } 134 | // Create a new tenant. 135 | if err := ts.WaitForInit(); err != nil { 136 | return nil, fmt.Errorf("%s WaitForInit failed: %w", tenantserverMessagePrefix, err) 137 | } 138 | pgURL := ts.PGURL() 139 | if pgURL == nil { 140 | return nil, fmt.Errorf("%s: url not found", tenantserverMessagePrefix) 141 | } 142 | db, err := sql.Open("postgres", pgURL.String()) 143 | if err != nil { 144 | return nil, fmt.Errorf("%s cannot open connection: %w", tenantserverMessagePrefix, err) 145 | } 146 | defer db.Close() 147 | if _, err := db.Exec(fmt.Sprintf("SELECT crdb_internal.create_tenant(%d)", tenantID)); err != nil { 148 | return nil, fmt.Errorf("%s cannot create tenant: %w", tenantserverMessagePrefix, err) 149 | } 150 | 151 | // TODO(asubiotto): We should pass ":0" as the sql addr to push port 152 | // selection to the cockroach mt start-sql command. However, that requires 153 | // that the mt start-sql command supports --listening-url-file so that this 154 | // test harness can subsequently read the postgres url. The current 155 | // approach is to do our best to find a free port and use that. 156 | addr := func() (string, error) { 157 | l, err := net.Listen("tcp", ":0") 158 | if err != nil { 159 | return "", fmt.Errorf("%s cannot listen on a port: %w", tenantserverMessagePrefix, err) 160 | } 161 | // Use localhost because of certificate validation issues otherwise 162 | // (something about IP SANs). 163 | addr := "localhost:" + strconv.Itoa(l.Addr().(*net.TCPAddr).Port) 164 | if err := l.Close(); err != nil { 165 | return "", fmt.Errorf("%s cannot close listener: %w", tenantserverMessagePrefix, err) 166 | } 167 | return addr, nil 168 | } 169 | sqlAddr, err := addr() 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | proxyAddr, err := func() (string, error) { 175 | <-ts.pgURL[0].set 176 | 177 | ts.mu.Lock() 178 | defer ts.mu.Unlock() 179 | if ts.proxyAddr != "" { 180 | return ts.proxyAddr, nil 181 | } 182 | var err error 183 | ts.proxyAddr, err = addr() 184 | if err != nil { 185 | return "", err 186 | } 187 | 188 | args := []string{ 189 | "mt", 190 | "start-proxy", 191 | "--listen-addr", 192 | ts.proxyAddr, 193 | "--routing-rule", 194 | sqlAddr, 195 | "--listen-cert", 196 | filepath.Join(certsDir, "node.crt"), 197 | "--listen-key", 198 | filepath.Join(certsDir, "node.key"), 199 | "--listen-metrics=:0", 200 | "--skip-verify", 201 | } 202 | cmd := exec.Command(cockroachBinary, args...) 203 | log.Printf("%s executing: %s", tenantserverMessagePrefix, cmd) 204 | if err := cmd.Start(); err != nil { 205 | return "", fmt.Errorf("%s command %s failed: %w", tenantserverMessagePrefix, cmd, err) 206 | } 207 | if cmd.Process != nil { 208 | log.Printf("%s: process %d started: %s", tenantserverMessagePrefix, cmd.Process.Pid, 209 | strings.Join(args, " ")) 210 | } 211 | ts.proxyProcess = cmd.Process 212 | 213 | return ts.proxyAddr, nil 214 | }() 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | args := []string{ 220 | cockroachBinary, 221 | "mt", 222 | "start-sql", 223 | secureFlag, 224 | "--logtostderr", 225 | fmt.Sprintf("--tenant-id=%d", tenantID), 226 | "--kv-addrs=" + pgURL.Host, 227 | "--sql-addr=" + sqlAddr, 228 | "--http-addr=:0", 229 | } 230 | 231 | nodes := []nodeInfo{ 232 | { 233 | state: stateNew, 234 | startCmdArgs: args, 235 | // TODO(asubiotto): Specify listeningURLFile once we support dynamic 236 | // ports. 237 | listeningURLFile: "", 238 | stdout: filepath.Join(ts.baseDir, logsDirName, fmt.Sprintf("cockroach.tenant.%d.stdout", tenantID)), 239 | stderr: filepath.Join(ts.baseDir, logsDirName, fmt.Sprintf("cockroach.tenant.%d.stderr", tenantID)), 240 | }, 241 | } 242 | 243 | tenant := &testServerImpl{ 244 | serverArgs: ts.serverArgs, 245 | version: ts.version, 246 | serverState: stateNew, 247 | baseDir: ts.baseDir, 248 | nodes: nodes, 249 | } 250 | 251 | // Start the tenant. 252 | // Initialize direct connection to the tenant. We need to use `orig` instead of `pgurl` because if the test server 253 | // is using a root password, this password does not carry over to the tenant; client certs will, though. 254 | tenantURL := ts.pgURL[0].orig 255 | tenantURL.Host = sqlAddr 256 | tenant.pgURL = make([]pgURLChan, 1) 257 | tenant.pgURL[0].started = make(chan struct{}) 258 | tenant.pgURL[0].set = make(chan struct{}) 259 | 260 | tenant.setPGURL(&tenantURL) 261 | if err := tenant.Start(); err != nil { 262 | return nil, fmt.Errorf("%s Start failed : %w", tenantserverMessagePrefix, err) 263 | } 264 | if err := tenant.WaitForInit(); err != nil { 265 | return nil, fmt.Errorf("%s WaitForInit failed: %w", tenantserverMessagePrefix, err) 266 | } 267 | 268 | tenantDB, err := sql.Open("postgres", tenantURL.String()) 269 | if err != nil { 270 | return nil, fmt.Errorf("%s cannot open connection: %w", tenantserverMessagePrefix, err) 271 | } 272 | defer tenantDB.Close() 273 | 274 | rootPassword := "" 275 | if proxy { 276 | // The proxy does not do client certs, so always set a password if we use the proxy. 277 | rootPassword = "admin" 278 | } 279 | if pw := ts.serverArgs.rootPW; pw != "" { 280 | rootPassword = pw 281 | } 282 | 283 | if rootPassword != "" { 284 | // Allow root to login via password. 285 | if _, err := tenantDB.Exec(`ALTER USER root WITH PASSWORD $1`, rootPassword); err != nil { 286 | return nil, fmt.Errorf("%s cannot set password: %w", tenantserverMessagePrefix, err) 287 | } 288 | 289 | // NB: need the lock since *tenantURL is owned by `tenant`. 290 | tenant.mu.Lock() 291 | v := tenantURL.Query() 292 | if proxy { 293 | // If using proxy, point url at the proxy instead of at the tenant directly. 294 | tenantURL.Host = proxyAddr 295 | // Massage the query string. The proxy expects the magic cluster name 'prancing-pony'. We remove the client 296 | // certs since we won't be using them (and they don't work through the proxy anyway). 297 | v.Add("options", "--cluster=prancing-pony-2") 298 | } 299 | 300 | // Client certs should not be used; we're using password auth. 301 | v.Del("sslcert") 302 | v.Del("sslkey") 303 | tenantURL.RawQuery = v.Encode() 304 | tenantURL.User = url.UserPassword("root", rootPassword) 305 | tenant.mu.Unlock() 306 | } 307 | 308 | return tenant, nil 309 | } 310 | -------------------------------------------------------------------------------- /crdb/retry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package crdb 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | // RetryFunc owns the state for a transaction retry operation. Usually, this is 22 | // just the retry count. RetryFunc is not assumed to be safe for concurrent use. 23 | // 24 | // The function is called after each retryable error to determine whether to 25 | // retry and how long to wait. It receives the retryable error that triggered 26 | // the retry attempt. 27 | // 28 | // Return values: 29 | // - duration: The delay to wait before the next retry attempt. If 0, retry 30 | // immediately without delay. 31 | // - error: If non-nil, stops retrying and returns this error to the caller 32 | // (typically a MaxRetriesExceededError). If nil, the retry will proceed 33 | // after the specified duration. 34 | // 35 | // Example behavior: 36 | // - (100ms, nil): Wait 100ms, then retry 37 | // - (0, nil): Retry immediately 38 | // - (0, err): Stop retrying, return err to caller 39 | type RetryFunc func(err error) (time.Duration, error) 40 | 41 | // RetryPolicy constructs a new instance of a RetryFunc for each transaction 42 | // it is used with. Instances of RetryPolicy can likely be immutable and 43 | // should be safe for concurrent calls to NewRetry. 44 | type RetryPolicy interface { 45 | NewRetry() RetryFunc 46 | } 47 | 48 | const ( 49 | // NoRetries is a sentinel value for LimitBackoffRetryPolicy.RetryLimit 50 | // indicating that no retries should be attempted. When a policy has 51 | // RetryLimit set to NoRetries, the transaction will be attempted only 52 | // once, and any retryable error will immediately return a 53 | // MaxRetriesExceededError. 54 | // 55 | // Use WithNoRetries(ctx) to create a context with this behavior. 56 | NoRetries = -1 57 | 58 | // UnlimitedRetries indicates that retries should continue indefinitely 59 | // until the transaction succeeds or a non-retryable error occurs. This 60 | // is represented by setting RetryLimit to 0. 61 | // 62 | // Use WithMaxRetries(ctx, 0) to create a context with unlimited retries, 63 | // though this is generally not recommended in production as it can lead 64 | // to infinite retry loops. 65 | UnlimitedRetries = 0 66 | ) 67 | 68 | // LimitBackoffRetryPolicy implements RetryPolicy with a configurable retry limit 69 | // and optional constant delay between retries. 70 | // 71 | // The RetryLimit field controls retry behavior: 72 | // - Positive value (e.g., 10): Retry up to that many times before failing 73 | // - UnlimitedRetries (0): Retry indefinitely until success or non-retryable error 74 | // - NoRetries (-1) or any negative value: Do not retry; fail immediately on first retryable error 75 | // 76 | // If Delay is greater than zero, the policy will wait for the specified duration 77 | // between retry attempts. 78 | // 79 | // Example usage with limited retries and no delay: 80 | // 81 | // policy := &LimitBackoffRetryPolicy{ 82 | // RetryLimit: 10, 83 | // Delay: 0, 84 | // } 85 | // ctx := crdb.WithRetryPolicy(context.Background(), policy) 86 | // err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 87 | // // transaction logic 88 | // }) 89 | // 90 | // Example usage with fixed delay between retries: 91 | // 92 | // policy := &LimitBackoffRetryPolicy{ 93 | // RetryLimit: 5, 94 | // Delay: 100 * time.Millisecond, 95 | // } 96 | // ctx := crdb.WithRetryPolicy(context.Background(), policy) 97 | // 98 | // Example usage with unlimited retries: 99 | // 100 | // policy := &LimitBackoffRetryPolicy{ 101 | // RetryLimit: UnlimitedRetries, // or 0 102 | // Delay: 50 * time.Millisecond, 103 | // } 104 | // 105 | // Note: Convenience functions are available: 106 | // - WithMaxRetries(ctx, n) creates a LimitBackoffRetryPolicy with RetryLimit=n and Delay=0 107 | // - WithNoRetries(ctx) creates a LimitBackoffRetryPolicy with RetryLimit=NoRetries 108 | type LimitBackoffRetryPolicy struct { 109 | // RetryLimit controls the retry behavior: 110 | // - Positive value: Maximum number of retries before returning MaxRetriesExceededError 111 | // - UnlimitedRetries (0): Retry indefinitely 112 | // - NoRetries (-1) or any negative value: Do not retry, fail immediately 113 | RetryLimit int 114 | 115 | // Delay is the fixed duration to wait between retry attempts. If 0, 116 | // retries happen immediately without delay. 117 | Delay time.Duration 118 | } 119 | 120 | // NewRetry implements RetryPolicy. 121 | func (l *LimitBackoffRetryPolicy) NewRetry() RetryFunc { 122 | tryCount := 0 123 | return func(err error) (time.Duration, error) { 124 | tryCount++ 125 | // Any negative value (including NoRetries) means fail immediately 126 | if l.RetryLimit < UnlimitedRetries { 127 | return 0, newMaxRetriesExceededError(err, 0) 128 | } 129 | // UnlimitedRetries (0) means retry indefinitely, so skip the limit check 130 | // Any positive value enforces the retry limit 131 | if l.RetryLimit > UnlimitedRetries && tryCount > l.RetryLimit { 132 | return 0, newMaxRetriesExceededError(err, l.RetryLimit) 133 | } 134 | return l.Delay, nil 135 | } 136 | } 137 | 138 | // ExpBackoffRetryPolicy implements RetryPolicy using an exponential backoff strategy 139 | // where delays double with each retry attempt, with an optional maximum delay cap. 140 | // 141 | // The delay between retries doubles with each attempt, starting from BaseDelay: 142 | // - Retry 1: BaseDelay 143 | // - Retry 2: BaseDelay * 2 144 | // - Retry 3: BaseDelay * 4 145 | // - Retry N: BaseDelay * 2^(N-1) 146 | // 147 | // If MaxDelay is set (> 0), the delay is capped at that value once reached. 148 | // This prevents excessive wait times during high retry counts and provides a 149 | // predictable upper bound for backoff duration. 150 | // 151 | // The RetryLimit field controls retry behavior: 152 | // - Positive value (e.g., 10): Retry up to that many times before failing 153 | // - UnlimitedRetries (0): Retry indefinitely until success or non-retryable error 154 | // - NoRetries (-1) or any negative value: Do not retry; fail immediately on first retryable error 155 | // 156 | // When the limit is exceeded or if the delay calculation overflows without a 157 | // MaxDelay set, it returns a MaxRetriesExceededError. 158 | // 159 | // Example usage with capped exponential backoff: 160 | // 161 | // policy := &ExpBackoffRetryPolicy{ 162 | // RetryLimit: 10, 163 | // BaseDelay: 100 * time.Millisecond, 164 | // MaxDelay: 5 * time.Second, 165 | // } 166 | // ctx := crdb.WithRetryPolicy(context.Background(), policy) 167 | // err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 168 | // // transaction logic that may encounter retryable errors 169 | // return tx.ExecContext(ctx, "UPDATE ...") 170 | // }) 171 | // 172 | // This configuration produces delays: 100ms, 200ms, 400ms, 800ms, 1.6s, 3.2s, 173 | // then stays at 5s for all subsequent retries. 174 | // 175 | // Example usage with unbounded exponential backoff: 176 | // 177 | // policy := &ExpBackoffRetryPolicy{ 178 | // RetryLimit: 5, 179 | // BaseDelay: 1 * time.Second, 180 | // MaxDelay: 0, // no cap 181 | // } 182 | // 183 | // This configuration produces delays: 1s, 2s, 4s, 8s, 16s. 184 | // Note: Setting MaxDelay to 0 means no cap, but be aware that delay overflow 185 | // will cause the policy to fail early. 186 | type ExpBackoffRetryPolicy struct { 187 | // RetryLimit controls the retry behavior: 188 | // - Positive value: Maximum number of retries before returning MaxRetriesExceededError 189 | // - UnlimitedRetries (0): Retry indefinitely 190 | // - NoRetries (-1) or any negative value: Do not retry, fail immediately 191 | RetryLimit int 192 | 193 | // BaseDelay is the initial delay before the first retry. Each subsequent 194 | // retry doubles this value: delay = BaseDelay * 2^(attempt-1). 195 | BaseDelay time.Duration 196 | 197 | // MaxDelay is the maximum delay cap. If > 0, delays are capped at this 198 | // value once reached. If 0, delays grow unbounded (until overflow, which 199 | // causes early termination). 200 | MaxDelay time.Duration 201 | } 202 | 203 | // NewRetry implements RetryPolicy. 204 | func (l *ExpBackoffRetryPolicy) NewRetry() RetryFunc { 205 | tryCount := 0 206 | return func(err error) (time.Duration, error) { 207 | tryCount++ 208 | // Any negative value (including NoRetries) means fail immediately 209 | if l.RetryLimit < UnlimitedRetries { 210 | return 0, newMaxRetriesExceededError(err, 0) 211 | } 212 | // UnlimitedRetries (0) means retry indefinitely, so skip the limit check 213 | // Any positive value enforces the retry limit 214 | if l.RetryLimit > UnlimitedRetries && tryCount > l.RetryLimit { 215 | return 0, newMaxRetriesExceededError(err, l.RetryLimit) 216 | } 217 | delay := l.BaseDelay << (tryCount - 1) 218 | if l.MaxDelay > 0 && delay > l.MaxDelay { 219 | return l.MaxDelay, nil 220 | } 221 | if delay < l.BaseDelay { 222 | // We've overflowed. 223 | if l.MaxDelay > 0 { 224 | return l.MaxDelay, nil 225 | } 226 | // There's no max delay. Giving up is probably better in 227 | // practice than using a 290-year MAX_INT delay. 228 | return 0, newMaxRetriesExceededError(err, tryCount) 229 | } 230 | return delay, nil 231 | } 232 | } 233 | 234 | // ExternalBackoffPolicy adapts third-party backoff strategies 235 | // (like those from github.com/sethvargo/go-retry) 236 | // into a RetryPolicy without creating a direct dependency on those libraries. 237 | // 238 | // This function allows you to use any backoff implementation that conforms to the 239 | // ExternalBackoff interface, providing flexibility to integrate external retry strategies 240 | // with CockroachDB transaction retries. 241 | // 242 | // Example usage with a hypothetical external backoff library: 243 | // 244 | // import retry "github.com/sethvargo/go-retry" 245 | // 246 | // // Create a retry policy using an external backoff strategy 247 | // policy := crdb.ExternalBackoffPolicy(func() crdb.ExternalBackoff { 248 | // // Fibonacci backoff: 1s, 1s, 2s, 3s, 5s, 8s... 249 | // return retry.NewFibonacci(1 * time.Second) 250 | // }) 251 | // ctx := crdb.WithRetryPolicy(context.Background(), policy) 252 | // err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 253 | // // transaction logic 254 | // }) 255 | // 256 | // The function parameter should return a fresh ExternalBackoff instance for each 257 | // transaction, as backoff state is not safe for concurrent use. 258 | func ExternalBackoffPolicy(fn func() ExternalBackoff) RetryPolicy { 259 | return &externalBackoffAdapter{ 260 | DelegateFactory: fn, 261 | } 262 | } 263 | 264 | // ExternalBackoff is an interface for external backoff strategies that provide 265 | // delays through a Next() method. This allows adaptation of backoff policies 266 | // from libraries like github.com/sethvargo/go-retry without creating a direct 267 | // dependency. 268 | // 269 | // Next returns the next backoff duration and a boolean indicating whether to 270 | // stop retrying. When stop is true, the retry loop terminates with a 271 | // MaxRetriesExceededError. 272 | type ExternalBackoff interface { 273 | // Next returns the next delay duration and whether to stop retrying. 274 | // When stop is true, no more retries will be attempted. 275 | Next() (next time.Duration, stop bool) 276 | } 277 | 278 | // externalBackoffAdapter adapts backoff policies in the style of github.com/sethvargo/go-retry. 279 | type externalBackoffAdapter struct { 280 | DelegateFactory func() ExternalBackoff 281 | } 282 | 283 | // NewRetry implements RetryPolicy by delegating to the external backoff strategy. 284 | // It creates a fresh backoff instance using DelegateFactory and wraps its Next() 285 | // method to conform to the RetryFunc signature. 286 | func (b *externalBackoffAdapter) NewRetry() RetryFunc { 287 | delegate := b.DelegateFactory() 288 | count := 0 289 | return func(err error) (time.Duration, error) { 290 | count++ 291 | d, stop := delegate.Next() 292 | if stop { 293 | return 0, newMaxRetriesExceededError(err, count) 294 | } 295 | return d, nil 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /crdb/tx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // Package crdb provides helpers for using CockroachDB in client 16 | // applications. 17 | package crdb 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "errors" 23 | "time" 24 | ) 25 | 26 | // Execute runs fn and retries it as needed. It is used to add retry handling to 27 | // the execution of a single statement. If a multi-statement transaction is 28 | // being run, use ExecuteTx instead. 29 | // 30 | // Retry handling for individual statements (implicit transactions) is usually 31 | // performed automatically on the CockroachDB SQL gateway. As such, use of this 32 | // function is generally not necessary. The exception to this rule is that 33 | // automatic retries for individual statements are disabled once CockroachDB 34 | // begins streaming results for the statements back to the client. By default, 35 | // result streaming does not begin until the size of the result being produced 36 | // for the client, including protocol overhead, exceeds 16KiB. As long as the 37 | // results of a single statement or batch of statements are known to stay clear 38 | // of this limit, the client does not need to worry about retries and should not 39 | // need to use this function. 40 | // 41 | // For more information about automatic transaction retries in CockroachDB, see 42 | // https://cockroachlabs.com/docs/stable/transactions.html#automatic-retries. 43 | // 44 | // NOTE: the supplied fn closure should not have external side effects beyond 45 | // changes to the database. 46 | // 47 | // fn must take care when wrapping errors returned from the database driver with 48 | // additional context. For example, if the SELECT statement fails in the 49 | // following snippet, the original retryable error will be masked by the call to 50 | // fmt.Errorf, and the transaction will not be automatically retried. 51 | // 52 | // crdb.Execute(func () error { 53 | // rows, err := db.QueryContext(ctx, "SELECT ...") 54 | // if err != nil { 55 | // return fmt.Errorf("scanning row: %s", err) 56 | // } 57 | // defer rows.Close() 58 | // for rows.Next() { 59 | // // ... 60 | // } 61 | // if err := rows.Err(); err != nil { 62 | // return fmt.Errorf("scanning row: %s", err) 63 | // } 64 | // return nil 65 | // }) 66 | // 67 | // Instead, add context by returning an error that implements either: 68 | // - a `Cause() error` method, in the manner of github.com/pkg/errors, or 69 | // - an `Unwrap() error` method, in the manner of the Go 1.13 standard 70 | // library. 71 | // 72 | // To achieve this, you can implement your own error type, or use 73 | // `errors.Wrap()` from github.com/pkg/errors or 74 | // github.com/cockroachdb/errors or a similar package, or use go 75 | // 1.13's special `%w` formatter with fmt.Errorf(), for example 76 | // fmt.Errorf("scanning row: %w", err). 77 | // 78 | // import "github.com/pkg/errors" 79 | // 80 | // crdb.Execute(func () error { 81 | // rows, err := db.QueryContext(ctx, "SELECT ...") 82 | // if err != nil { 83 | // return errors.Wrap(err, "scanning row") 84 | // } 85 | // defer rows.Close() 86 | // for rows.Next() { 87 | // // ... 88 | // } 89 | // if err := rows.Err(); err != nil { 90 | // return errors.Wrap(err, "scanning row") 91 | // } 92 | // return nil 93 | // }) 94 | func Execute(fn func() error) (err error) { 95 | for { 96 | err = fn() 97 | if err == nil || !errIsRetryable(err) { 98 | return err 99 | } 100 | } 101 | } 102 | 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 retry policy 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 | // Arbitrary retry policies can be configured using WithRetryPolicy(ctx, p). 120 | // 121 | // The fn parameter accepts variadic arguments which are passed through on each 122 | // retry attempt, allowing for flexible parameterization of the retried operation. 123 | // 124 | // As with Execute, retry handling for individual statements (implicit transactions) 125 | // is usually performed automatically on the CockroachDB SQL gateway, making use 126 | // of this function generally unnecessary. However, automatic retries are disabled 127 | // once result streaming begins (typically when results exceed 16KiB). 128 | // 129 | // NOTE: the supplied fn closure should not have external side effects beyond 130 | // changes to the database. 131 | // 132 | // fn must take care when wrapping errors returned from the database driver with 133 | // additional context. To preserve retry behavior, errors should implement either 134 | // `Cause() error` (github.com/pkg/errors) or `Unwrap() error` (Go 1.13+). 135 | // For example: 136 | // 137 | // crdb.ExecuteCtx(ctx, func(ctx context.Context, args ...interface{}) error { 138 | // id := args[0].(int) 139 | // rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id) 140 | // if err != nil { 141 | // return fmt.Errorf("scanning row: %w", err) // uses %w for proper error wrapping 142 | // } 143 | // defer rows.Close() 144 | // // ... 145 | // return nil 146 | // }, userID) 147 | func ExecuteCtx(ctx context.Context, fn ExecuteCtxFunc, args ...interface{}) (err error) { 148 | // establish the retry policy 149 | retryPolicy := getRetryPolicy(ctx) 150 | // set up the retry policy state 151 | retryFunc := retryPolicy.NewRetry() 152 | for { 153 | if err = ctx.Err(); err != nil { 154 | return err 155 | } 156 | 157 | err = fn(ctx, args...) 158 | if err == nil || !errIsRetryable(err) { 159 | return err 160 | } 161 | delay, retryErr := retryFunc(err) 162 | if retryErr != nil { 163 | return retryErr 164 | } 165 | if delay > 0 { 166 | select { 167 | case <-time.After(delay): 168 | case <-ctx.Done(): 169 | return ctx.Err() 170 | } 171 | } 172 | } 173 | } 174 | 175 | type txConfigKey struct{} 176 | 177 | // WithMaxRetries configures context so that ExecuteTx retries the transaction 178 | // up to the specified number of times when encountering retryable errors. 179 | // 180 | // The retries parameter controls retry behavior: 181 | // - Positive value (e.g., 10): Retry up to that many times before failing 182 | // - 0 (UnlimitedRetries): Retry indefinitely until success or non-retryable error 183 | // (not recommended in production as it can lead to infinite retry loops) 184 | // 185 | // This is a convenience function that creates a LimitBackoffRetryPolicy with 186 | // no delay between retries (immediate retries). 187 | // 188 | // Example with limited retries: 189 | // 190 | // ctx := crdb.WithMaxRetries(context.Background(), 10) 191 | // err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 192 | // // Will retry up to 10 times on retryable errors 193 | // return tx.ExecContext(ctx, "UPDATE ...") 194 | // }) 195 | // 196 | // Example with unlimited retries (use with caution): 197 | // 198 | // ctx := crdb.WithMaxRetries(context.Background(), 0) 199 | // // Will retry indefinitely - ensure you have a context timeout! 200 | // 201 | // To disable retries entirely, use WithNoRetries(ctx) instead. 202 | func WithMaxRetries(ctx context.Context, retries int) context.Context { 203 | p := &LimitBackoffRetryPolicy{retries, 0} 204 | return WithRetryPolicy(ctx, p) 205 | } 206 | 207 | // WithNoRetries configures context so that ExecuteTx will not retry on 208 | // retryable errors. The transaction will be attempted exactly once. 209 | // 210 | // This is useful when you want to handle retries manually or when operating 211 | // in a context where automatic retries are not desired (e.g., in testing, 212 | // or when implementing custom retry logic). 213 | // 214 | // Example usage: 215 | // 216 | // ctx := crdb.WithNoRetries(context.Background()) 217 | // err := crdb.ExecuteTx(ctx, db, nil, func(tx *sql.Tx) error { 218 | // // This will execute only once, no automatic retries 219 | // return tx.ExecContext(ctx, "UPDATE ...") 220 | // }) 221 | // if err != nil { 222 | // // Handle error manually, potentially implementing custom retry logic 223 | // } 224 | func WithNoRetries(ctx context.Context) context.Context { 225 | p := &LimitBackoffRetryPolicy{NoRetries, 0} 226 | return WithRetryPolicy(ctx, p) 227 | } 228 | 229 | // WithRetryPolicy uses an arbitrary retry policy to perform retries. 230 | func WithRetryPolicy(ctx context.Context, policy RetryPolicy) context.Context { 231 | return context.WithValue(ctx, txConfigKey{}, policy) 232 | } 233 | 234 | // getRetryPolicy retrieves the RetryPolicy from the context or the default 235 | func getRetryPolicy(ctx context.Context) RetryPolicy { 236 | retryPolicy := defaultRetryPolicy 237 | if v := ctx.Value(txConfigKey{}); v != nil { 238 | retryPolicy = v.(RetryPolicy) 239 | } 240 | 241 | return retryPolicy 242 | } 243 | 244 | const defaultRetries = 50 245 | 246 | var defaultRetryPolicy RetryPolicy = &LimitBackoffRetryPolicy{ 247 | RetryLimit: defaultRetries, 248 | } 249 | 250 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 251 | // non-retryable failures, the transaction is aborted and rolled back; on 252 | // success, the transaction is committed. 253 | // 254 | // There are cases where the state of a transaction is inherently ambiguous: if 255 | // we err on RELEASE with a communication error it's unclear if the transaction 256 | // has been committed or not (similar to erroring on COMMIT in other databases). 257 | // In that case, we return AmbiguousCommitError. 258 | // 259 | // There are cases when restarting a transaction fails: we err on ROLLBACK to 260 | // the SAVEPOINT. In that case, we return a TxnRestartError. 261 | // 262 | // For more information about CockroachDB's transaction model, see 263 | // https://cockroachlabs.com/docs/stable/transactions.html. 264 | // 265 | // NOTE: the supplied fn closure should not have external side effects beyond 266 | // changes to the database. 267 | // 268 | // fn must take care when wrapping errors returned from the database driver with 269 | // additional context. For example, if the UPDATE statement fails in the 270 | // following snippet, the original retryable error will be masked by the call to 271 | // fmt.Errorf, and the transaction will not be automatically retried. 272 | // 273 | // crdb.ExecuteTx(ctx, db, txopts, func (tx *sql.Tx) error { 274 | // if err := tx.ExecContext(ctx, "UPDATE..."); err != nil { 275 | // return fmt.Errorf("updating record: %s", err) 276 | // } 277 | // return nil 278 | // }) 279 | // 280 | // Instead, add context by returning an error that implements either: 281 | // - a `Cause() error` method, in the manner of github.com/pkg/errors, or 282 | // - an `Unwrap() error` method, in the manner of the Go 1.13 standard 283 | // library. 284 | // 285 | // To achieve this, you can implement your own error type, or use 286 | // `errors.Wrap()` from github.com/pkg/errors or 287 | // github.com/cockroachdb/errors or a similar package, or use go 288 | // 1.13's special `%w` formatter with fmt.Errorf(), for example 289 | // fmt.Errorf("scanning row: %w", err). 290 | // 291 | // import "github.com/pkg/errors" 292 | // 293 | // crdb.ExecuteTx(ctx, db, txopts, func (tx *sql.Tx) error { 294 | // if err := tx.ExecContext(ctx, "UPDATE..."); err != nil { 295 | // return errors.Wrap(err, "updating record") 296 | // } 297 | // return nil 298 | // }) 299 | func ExecuteTx(ctx context.Context, db *sql.DB, opts *sql.TxOptions, fn func(*sql.Tx) error) error { 300 | // Start a transaction. 301 | tx, err := db.BeginTx(ctx, opts) 302 | if err != nil { 303 | return err 304 | } 305 | return ExecuteInTx(ctx, stdlibTxnAdapter{tx}, func() error { return fn(tx) }) 306 | } 307 | 308 | type stdlibTxnAdapter struct { 309 | tx *sql.Tx 310 | } 311 | 312 | var _ Tx = stdlibTxnAdapter{} 313 | 314 | // Exec is part of the tx interface. 315 | func (tx stdlibTxnAdapter) Exec(ctx context.Context, q string, args ...interface{}) error { 316 | _, err := tx.tx.ExecContext(ctx, q, args...) 317 | return err 318 | } 319 | 320 | // Commit is part of the tx interface. 321 | func (tx stdlibTxnAdapter) Commit(context.Context) error { 322 | return tx.tx.Commit() 323 | } 324 | 325 | // Rollback is part of the tx interface. 326 | func (tx stdlibTxnAdapter) Rollback(context.Context) error { 327 | return tx.tx.Rollback() 328 | } 329 | 330 | func errIsRetryable(err error) bool { 331 | // We look for either: 332 | // - the standard PG errcode SerializationFailureError:40001 or 333 | // - the Cockroach extension errcode RetriableError:CR000. This extension 334 | // has been removed server-side, but support for it has been left here for 335 | // now to maintain backwards compatibility. 336 | code := errCode(err) 337 | return code == "CR000" || code == "40001" 338 | } 339 | 340 | func errCode(err error) string { 341 | var sqlErr errWithSQLState 342 | if errors.As(err, &sqlErr) { 343 | return sqlErr.SQLState() 344 | } 345 | 346 | return "" 347 | } 348 | 349 | // errWithSQLState is implemented by pgx (pgconn.PgError) and lib/pq 350 | type errWithSQLState interface { 351 | SQLState() string 352 | } 353 | -------------------------------------------------------------------------------- /testserver/binaries.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package testserver 16 | 17 | import ( 18 | "archive/tar" 19 | "archive/zip" 20 | "bytes" 21 | "compress/gzip" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "log" 26 | "net/http" 27 | "net/url" 28 | "os" 29 | "path" 30 | "path/filepath" 31 | "runtime" 32 | "strings" 33 | "time" 34 | 35 | "github.com/cockroachdb/cockroach-go/v2/testserver/version" 36 | "github.com/gofrs/flock" 37 | "gopkg.in/yaml.v3" 38 | ) 39 | 40 | const ( 41 | latestSuffix = "LATEST" 42 | finishedFileMode = 0o555 43 | writingFileMode = 0o600 // Allow reads so that another process can check if there's a flock. 44 | ) 45 | 46 | const ( 47 | linuxUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.linux-%s.tgz" 48 | macUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.darwin-%s-%s.tgz" 49 | winUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.windows-6.2-amd64.zip" 50 | ) 51 | 52 | // releaseDataURL is the location of the YAML file maintained by the 53 | // docs team where release information is encoded. This data is used 54 | // to render the public CockroachDB releases page. We leverage the 55 | // data in structured format to generate release information used 56 | // for testing purposes. 57 | const releaseDataURL = "https://raw.githubusercontent.com/cockroachdb/docs/main/src/current/_data/releases.yml" 58 | 59 | // GetDownloadURL returns the URL of a CRDB download. It creates the URL for 60 | // downloading a CRDB binary for current runtime OS. If desiredVersion is 61 | // specified, it will return the URL of the specified version. Otherwise, it 62 | // will return the URL of the latest stable cockroach binary. If nonStable is 63 | // true, the latest cockroach binary will be used. 64 | func GetDownloadURL(desiredVersion string, nonStable bool) (string, string, error) { 65 | return GetDownloadURLWithPlatform(desiredVersion, nonStable, runtime.GOOS, runtime.GOARCH) 66 | } 67 | 68 | // GetDownloadURLWithPlatform returns the URL of a CRDB download for the specified 69 | // platform and architecture. If desiredVersion is specified, it will return the URL 70 | // of the specified version. Otherwise, it will return the URL of the latest stable 71 | // cockroach binary. If nonStable is true, the latest cockroach binary will be used. 72 | func GetDownloadURLWithPlatform( 73 | desiredVersion string, nonStable bool, goos, goarch string, 74 | ) (string, string, error) { 75 | targetGoos := goos 76 | if targetGoos == "linux" { 77 | targetGoos += "-gnu" 78 | } 79 | // For unstable builds, macOS ARM64 binaries have ".unsigned" at the end 80 | var binaryName string 81 | if nonStable && goos == "darwin" && goarch == "arm64" { 82 | binaryName = fmt.Sprintf("cockroach.%s-%s.unsigned", targetGoos, goarch) 83 | } else { 84 | binaryName = fmt.Sprintf("cockroach.%s-%s", targetGoos, goarch) 85 | } 86 | if goos == "windows" { 87 | binaryName += ".exe" 88 | } 89 | 90 | var dbUrl string 91 | var err error 92 | 93 | if desiredVersion != "" { 94 | dbUrl = getDownloadUrlForVersionWithPlatform(desiredVersion, goos, goarch) 95 | } else if nonStable { 96 | // For the latest (beta) CRDB, we use the `edge-binaries.cockroachdb.com` host. 97 | u := &url.URL{ 98 | Scheme: "https", 99 | Host: "edge-binaries.cockroachdb.com", 100 | Path: path.Join("cockroach", fmt.Sprintf("%s.%s", binaryName, latestSuffix)), 101 | } 102 | dbUrl = u.String() 103 | } else { 104 | // For the latest stable CRDB, we use the url provided in the CRDB release page. 105 | dbUrl, desiredVersion, err = getLatestStableVersionInfo() 106 | if err != nil { 107 | return dbUrl, "", err 108 | } 109 | } 110 | 111 | return dbUrl, desiredVersion, nil 112 | } 113 | 114 | // DownloadFromURL starts a download of the cockroach binary from the given URL. 115 | func DownloadFromURL(downloadURL string) (*http.Response, error) { 116 | log.Printf("GET %s", downloadURL) 117 | response, err := http.Get(downloadURL) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | if response.StatusCode != 200 { 123 | return nil, fmt.Errorf( 124 | "error downloading %s: %d (%s)", 125 | downloadURL, 126 | response.StatusCode, 127 | response.Status, 128 | ) 129 | } 130 | 131 | return response, nil 132 | } 133 | 134 | // DownloadBinary saves the latest version of CRDB into a local binary file, 135 | // and returns the path for this local binary. 136 | // To download a specific cockroach version, specify desiredVersion. Otherwise, 137 | // the latest stable or non-stable version will be chosen. 138 | // To download the latest STABLE version of CRDB, set `nonStable` to false. 139 | // To download the bleeding edge version of CRDB, set `nonStable` to true. 140 | func DownloadBinary(tc *TestConfig, desiredVersion string, nonStable bool) (string, error) { 141 | return DownloadBinaryWithPlatform(tc, desiredVersion, nonStable, runtime.GOOS, runtime.GOARCH, "") 142 | } 143 | 144 | // DownloadBinaryWithPlatform saves the specified version of CRDB for the given 145 | // platform and architecture into a local binary file, and returns the path for 146 | // this local binary. 147 | // To download a specific cockroach version, specify desiredVersion. Otherwise, 148 | // the latest stable or non-stable version will be chosen. 149 | // To download the latest STABLE version of CRDB, set `nonStable` to false. 150 | // To download the bleeding edge version of CRDB, set `nonStable` to true. 151 | // If outputDir is specified, the binary will be saved there, otherwise to temp directory. 152 | func DownloadBinaryWithPlatform( 153 | tc *TestConfig, desiredVersion string, nonStable bool, goos, goarch, outputDir string, 154 | ) (string, error) { 155 | dbUrl, desiredVersion, err := GetDownloadURLWithPlatform(desiredVersion, nonStable, goos, goarch) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | // For unstable builds, use "latest" as the version for filename generation 161 | filenameVersion := desiredVersion 162 | if nonStable && desiredVersion == "" { 163 | filenameVersion = "latest" 164 | } 165 | 166 | filename, err := GetDownloadFilenameWithPlatform(filenameVersion, goos) 167 | if err != nil { 168 | return "", err 169 | } 170 | 171 | var localFile string 172 | if outputDir != "" { 173 | // Create output directory if it doesn't exist 174 | if err := os.MkdirAll(outputDir, 0o755); err != nil { 175 | return "", fmt.Errorf("failed to create output directory %s: %w", outputDir, err) 176 | } 177 | localFile = filepath.Join(outputDir, filename) 178 | } else { 179 | localFile = filepath.Join(os.TempDir(), filename) 180 | } 181 | 182 | // Short circuit if the file already exists and is in the finished state. 183 | info, err := os.Stat(localFile) 184 | if err == nil && info.Mode().Perm() == finishedFileMode { 185 | return localFile, nil 186 | } 187 | 188 | response, err := DownloadFromURL(dbUrl) 189 | if err != nil { 190 | return "", err 191 | } 192 | 193 | defer func() { _ = response.Body.Close() }() 194 | 195 | for { 196 | info, err := os.Stat(localFile) 197 | if os.IsNotExist(err) { 198 | // File does not exist: download it. 199 | break 200 | } 201 | if err != nil { 202 | return "", err 203 | } 204 | // File already present: check mode. 205 | if info.Mode().Perm() == finishedFileMode { 206 | return localFile, nil 207 | } 208 | 209 | localFileLock := flock.New(localFile) 210 | // If there's a process downloading the binary, local file cannot be flocked. 211 | locked, err := localFileLock.TryLock() 212 | if err != nil { 213 | return "", err 214 | } 215 | 216 | if locked { 217 | // If local file can be locked, it means the previous download was 218 | // killed in the middle. Delete local file and re-download. 219 | log.Printf("previous download failed in the middle, deleting and re-downloading") 220 | if err := os.Remove(localFile); err != nil { 221 | log.Printf("failed to remove partial download %s: %v", localFile, err) 222 | return "", err 223 | } 224 | break 225 | } 226 | 227 | log.Printf("waiting for download of %s", localFile) 228 | time.Sleep(time.Millisecond * 10) 229 | } 230 | 231 | output, err := os.OpenFile(localFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, writingFileMode) 232 | if err != nil { 233 | return "", fmt.Errorf("error creating %s: %w", localFile, err) 234 | } 235 | 236 | // Assign a flock to the local file. 237 | // If the downloading process is killed in the middle, 238 | // the lock will be automatically dropped. 239 | localFileLock := flock.New(localFile) 240 | 241 | if _, err := localFileLock.TryLock(); err != nil { 242 | return "", err 243 | } 244 | 245 | defer func() { _ = localFileLock.Unlock() }() 246 | 247 | if tc.IsTest && tc.StopDownloadInMiddle { 248 | log.Printf("download process killed") 249 | output.Close() 250 | return "", errStoppedInMiddle 251 | } 252 | 253 | var downloadMethod func(*http.Response, *os.File, string) error 254 | 255 | if nonStable { 256 | downloadMethod = downloadBinaryFromResponse 257 | } else { 258 | if goos == "windows" { 259 | downloadMethod = downloadBinaryFromZip 260 | } else { 261 | downloadMethod = downloadBinaryFromTar 262 | } 263 | } 264 | log.Printf("saving %s to %s, this may take some time", response.Request.URL, localFile) 265 | if err := downloadMethod(response, output, localFile); err != nil { 266 | if !errors.Is(err, errStoppedInMiddle) { 267 | if err := os.Remove(localFile); err != nil { 268 | log.Printf("failed to remove %s: %s", localFile, err) 269 | } 270 | } 271 | return "", err 272 | } 273 | 274 | if err := localFileLock.Unlock(); err != nil { 275 | return "", err 276 | } 277 | 278 | if err := output.Close(); err != nil { 279 | return "", err 280 | } 281 | 282 | return localFile, nil 283 | } 284 | 285 | // GetDownloadFilename returns the local filename of the downloaded CRDB binary file. 286 | func GetDownloadFilename(desiredVersion string) (string, error) { 287 | return GetDownloadFilenameWithPlatform(desiredVersion, runtime.GOOS) 288 | } 289 | 290 | // GetDownloadFilenameWithPlatform returns the local filename of the downloaded CRDB binary file 291 | // for the specified platform. 292 | func GetDownloadFilenameWithPlatform(desiredVersion, goos string) (string, error) { 293 | filename := fmt.Sprintf("cockroach-%s", desiredVersion) 294 | if goos == "windows" { 295 | filename += ".exe" 296 | } 297 | return filename, nil 298 | } 299 | 300 | // Release contains the information we extract from the YAML file in 301 | // `releaseDataURL`. 302 | type Release struct { 303 | Name string `yaml:"release_name"` 304 | Withdrawn bool `yaml:"withdrawn"` 305 | CloudOnly bool `yaml:"cloud_only"` 306 | } 307 | 308 | // getLatestStableVersionInfo returns the latest stable CRDB's download URL, 309 | // and the formatted corresponding version number. The download URL is based 310 | // on the runtime OS. 311 | // Note that it may return a withdrawn version, but the risk is low for local tests here. 312 | func getLatestStableVersionInfo() (string, string, error) { 313 | resp, err := http.Get(releaseDataURL) 314 | if err != nil { 315 | return "", "", fmt.Errorf("could not download release data: %w", err) 316 | } 317 | defer resp.Body.Close() 318 | 319 | var blob bytes.Buffer 320 | if _, err := io.Copy(&blob, resp.Body); err != nil { 321 | return "", "", fmt.Errorf("error reading response body: %w", err) 322 | } 323 | 324 | var data []Release 325 | if err := yaml.Unmarshal(blob.Bytes(), &data); err != nil { //nolint:yaml 326 | return "", "", fmt.Errorf("failed to YAML parse release data: %w", err) 327 | } 328 | 329 | latestStableVersion := version.MustParse("v0.0.0") 330 | 331 | for _, r := range data { 332 | // We ignore versions that cannot be parsed; this should 333 | // correspond to really old beta releases. 334 | v, err := version.Parse(r.Name) 335 | if err != nil { 336 | continue 337 | } 338 | 339 | // Skip cloud-only releases, since they cannot be downloaded from 340 | // binaries.cockroachdb.com. 341 | if r.CloudOnly { 342 | continue 343 | } 344 | 345 | // Ignore any withdrawn releases, since they are known to be broken. 346 | if r.Withdrawn { 347 | continue 348 | } 349 | 350 | // Ignore alphas, betas, and RCs. 351 | if v.PreRelease() != "" { 352 | continue 353 | } 354 | 355 | if v.Compare(latestStableVersion) > 0 { 356 | latestStableVersion = v 357 | } 358 | } 359 | 360 | downloadUrl := getDownloadUrlForVersionWithPlatform(latestStableVersion.String(), runtime.GOOS, runtime.GOARCH) 361 | 362 | latestStableVerFormatted := strings.ReplaceAll(latestStableVersion.String(), ".", "-") 363 | return downloadUrl, latestStableVerFormatted, nil 364 | } 365 | 366 | func getDownloadUrlForVersionWithPlatform(version, goos, goarch string) string { 367 | switch goos { 368 | case "linux": 369 | return fmt.Sprintf(linuxUrlpat, version, goarch) 370 | case "darwin": 371 | switch goarch { 372 | case "arm64": 373 | return fmt.Sprintf(macUrlpat, version, "11.0", goarch) 374 | case "amd64": 375 | return fmt.Sprintf(macUrlpat, version, "10.9", goarch) 376 | } 377 | case "windows": 378 | return fmt.Sprintf(winUrlpat, version) 379 | } 380 | 381 | panic(fmt.Errorf("unsupported platform/architecture combination: %s-%s", goos, goarch)) 382 | } 383 | 384 | // downloadBinaryFromResponse copies the http response's body directly into a local binary. 385 | func downloadBinaryFromResponse(response *http.Response, output *os.File, filePath string) error { 386 | if _, err := io.Copy(output, response.Body); err != nil { 387 | return fmt.Errorf("problem saving %s to %s: %w", response.Request.URL, filePath, err) 388 | } 389 | 390 | // Download was successful, add the rw bits. 391 | if err := output.Chmod(finishedFileMode); err != nil { 392 | return err 393 | } 394 | 395 | return nil 396 | } 397 | 398 | // downloadBinaryFromTar writes the binary compressed in a tar from a http response 399 | // to a local file. 400 | // It is created because the download url from the release page only provides the tar.gz/zip 401 | // for a pre-compiled binary. 402 | func downloadBinaryFromTar(response *http.Response, output *os.File, filePath string) error { 403 | // Unzip the tar file from the response's body. 404 | gzf, err := gzip.NewReader(response.Body) 405 | if err != nil { 406 | return fmt.Errorf("cannot read tar from response body: %w", err) 407 | } 408 | // Read the files from the tar. 409 | tarReader := tar.NewReader(gzf) 410 | for { 411 | header, err := tarReader.Next() 412 | 413 | // No more file from tar to read. 414 | if err == io.EOF { 415 | return fmt.Errorf("cannot find the binary from tar") 416 | } 417 | 418 | if err != nil { 419 | return fmt.Errorf("cannot untar: %w", err) 420 | } 421 | 422 | // Only copy the cockroach binary. 423 | // The header.Name is of the form "zip_name/file_name". 424 | // We extract the file name. 425 | splitHeaderName := strings.Split(header.Name, "/") 426 | fileName := splitHeaderName[len(splitHeaderName)-1] 427 | if fileName == "cockroach" { 428 | // Copy the binary to desired path. 429 | if _, err := io.Copy(output, tarReader); err != nil { 430 | return fmt.Errorf( 431 | "problem saving %s to %s: %w", 432 | response.Request.URL, filePath, 433 | err, 434 | ) 435 | } 436 | if err := output.Chmod(finishedFileMode); err != nil { 437 | return err 438 | } 439 | return nil 440 | } 441 | 442 | } 443 | // Unreachable, but left present for safety in case later changes make this branch reachable again. 444 | return fmt.Errorf("could not find cockroach binary in archive") 445 | } 446 | 447 | // downloadBinaryFromZip writes the binary compressed in a zip from a http response 448 | // to a local file. 449 | // It is created because the download url from the release page only provides the tar.gz/zip 450 | // for a pre-compiled binary. 451 | func downloadBinaryFromZip(response *http.Response, output *os.File, filePath string) error { 452 | body, err := io.ReadAll(response.Body) 453 | if err != nil { 454 | return fmt.Errorf("cannot read zip from response body: %w", err) 455 | } 456 | 457 | zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) 458 | if err != nil { 459 | log.Fatal(err) 460 | } 461 | 462 | findFile := false 463 | // Read all the files from zip archive. 464 | for _, zipFile := range zipReader.File { 465 | splitHeaderName := strings.Split(zipFile.Name, "/") 466 | fileName := splitHeaderName[len(splitHeaderName)-1] 467 | fmt.Printf("filename=%s", fileName) 468 | if fileName == "cockroach" { 469 | findFile = true 470 | if err := readZipFile(zipFile, output); err != nil { 471 | return fmt.Errorf("problem saving %s to %s: %w", 472 | response.Request.URL, 473 | filePath, 474 | err) 475 | } 476 | if err := output.Chmod(finishedFileMode); err != nil { 477 | return err 478 | } 479 | } 480 | } 481 | if !findFile { 482 | return fmt.Errorf("cannot find the binary from zip") 483 | } 484 | 485 | return nil 486 | } 487 | 488 | func readZipFile(zf *zip.File, target *os.File) error { 489 | f, err := zf.Open() 490 | if err != nil { 491 | return err 492 | } 493 | defer f.Close() 494 | if _, err = io.Copy(target, f); err != nil { 495 | return err 496 | } 497 | return nil 498 | } 499 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 6 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 7 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 8 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 9 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 14 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 15 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 16 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 17 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 18 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 19 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 20 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 21 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 22 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 23 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 24 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 25 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 26 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 27 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 28 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 29 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 30 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 31 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 32 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 33 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 34 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 35 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 36 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 37 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 38 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 39 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 40 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 41 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 42 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 43 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 44 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 45 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 46 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 47 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 48 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 49 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 50 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 51 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 52 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 53 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 54 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 55 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 56 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 57 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 58 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 59 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 60 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 61 | github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= 62 | github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= 63 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 64 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 65 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 66 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 67 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 68 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 69 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 70 | github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= 71 | github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 72 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 73 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 74 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 75 | github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= 76 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 77 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 78 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 79 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 80 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 81 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 82 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 83 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 84 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 85 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 86 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 87 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 88 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 89 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 90 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 91 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 92 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 93 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 94 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 95 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 96 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 97 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 98 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 99 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 100 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 101 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 102 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 103 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 104 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 105 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 106 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 107 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 108 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 109 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 110 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 111 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 112 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 113 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 114 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 115 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 116 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 117 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 118 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 119 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 120 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 121 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 122 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 123 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 124 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 125 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 126 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 127 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 128 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 129 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 130 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 131 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 132 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 133 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 134 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 135 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 136 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 137 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 138 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 139 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 140 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 141 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 142 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 143 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 144 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 145 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 146 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 147 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 148 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 149 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 150 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 151 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 152 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 153 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 154 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 155 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 156 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 157 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 158 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 159 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 160 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 161 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 162 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 163 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 164 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 165 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 166 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 167 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 168 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 169 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 170 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 171 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 176 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 177 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 178 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 179 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 180 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 184 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 185 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 186 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 195 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 199 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 200 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 201 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 202 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 203 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 204 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 205 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 206 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 207 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 208 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 209 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 210 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 211 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 212 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 213 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 214 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 215 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 216 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 217 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 218 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 219 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 220 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 221 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 222 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 223 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 224 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 225 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 226 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 227 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 228 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 229 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 230 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 231 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 232 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 233 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 234 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 235 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 236 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 237 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 238 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 239 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 240 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 241 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 242 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 243 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 244 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 245 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 246 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 247 | gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= 248 | gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 249 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 250 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 251 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 252 | -------------------------------------------------------------------------------- /testserver/testserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package testserver_test 16 | 17 | import ( 18 | "database/sql" 19 | "fmt" 20 | "io" 21 | "log" 22 | "net" 23 | http "net/http" 24 | "os" 25 | "os/exec" 26 | "path/filepath" 27 | "runtime" 28 | "strconv" 29 | "strings" 30 | "sync" 31 | "testing" 32 | "time" 33 | 34 | "github.com/cockroachdb/cockroach-go/v2/testserver" 35 | "github.com/stretchr/testify/require" 36 | ) 37 | 38 | const noPW = "" 39 | 40 | const defStoreMemSize = 0.2 41 | 42 | func TestRunServer(t *testing.T) { 43 | const testPW = "foobar" 44 | for _, tc := range []struct { 45 | name string 46 | instantiation func(*testing.T) (*sql.DB, func()) 47 | }{ 48 | { 49 | name: "Insecure", 50 | instantiation: func(t *testing.T) (*sql.DB, func()) { return testserver.NewDBForTest(t) }, 51 | }, 52 | { 53 | name: "InsecureWithHostOverride", 54 | instantiation: func(t *testing.T) (*sql.DB, func()) { 55 | return testserver.NewDBForTest(t, testserver.ListenAddrHostOpt("0.0.0.0")) 56 | }, 57 | }, 58 | { 59 | name: "InsecureWithCustomizedMemSize", 60 | instantiation: func(t *testing.T) (*sql.DB, func()) { 61 | return testserver.NewDBForTest(t, testserver.SetStoreMemSizeOpt(0.3)) 62 | }, 63 | }, 64 | { 65 | name: "SecureClientCert", 66 | instantiation: func(t *testing.T) (*sql.DB, func()) { return testserver.NewDBForTest(t, testserver.SecureOpt()) }, 67 | }, 68 | { 69 | name: "SecurePassword", 70 | instantiation: func(t *testing.T) (*sql.DB, func()) { 71 | return testserver.NewDBForTest(t, testserver.SecureOpt(), testserver.RootPasswordOpt(testPW)) 72 | }, 73 | }, 74 | { 75 | name: "InsecureTenantStoreOnDisk", 76 | instantiation: func(t *testing.T) (*sql.DB, func()) { 77 | return testserver.NewDBForTest(t, testserver.StoreOnDiskOpt()) 78 | }, 79 | }, 80 | { 81 | name: "SecureTenantStoreOnDisk", 82 | instantiation: func(t *testing.T) (*sql.DB, func()) { 83 | return testserver.NewDBForTest(t, testserver.SecureOpt(), testserver.StoreOnDiskOpt()) 84 | }, 85 | }, 86 | { 87 | name: "InsecureTenant", 88 | instantiation: func(t *testing.T) (*sql.DB, func()) { 89 | return newTenantDBForTest( 90 | t, 91 | false, /* secure */ 92 | false, /* proxy */ 93 | noPW, /* pw */ 94 | false, /* diskStore */ 95 | defStoreMemSize, /* storeMemSize */ 96 | false, /* nonStableDB */ 97 | ) 98 | }, 99 | }, 100 | { 101 | name: "SecureTenant", 102 | instantiation: func(t *testing.T) (*sql.DB, func()) { 103 | return newTenantDBForTest( 104 | t, 105 | true, /* secure */ 106 | false, /* proxy */ 107 | noPW, /* pw */ 108 | false, /* diskStore */ 109 | defStoreMemSize, /* storeMemSize */ 110 | false, /* nonStableDB */ 111 | ) 112 | }, 113 | }, 114 | { 115 | name: "SecureTenantCustomPassword", 116 | instantiation: func(t *testing.T) (*sql.DB, func()) { 117 | return newTenantDBForTest(t, 118 | true, /* secure */ 119 | false, /* proxy */ 120 | testPW, /* pw */ 121 | false, /* diskStore */ 122 | defStoreMemSize, /* storeMemSize */ 123 | false, /* nonStableDB */ 124 | ) 125 | }, 126 | }, 127 | { 128 | name: "InsecureNonStable", 129 | instantiation: func(t *testing.T) (*sql.DB, func()) { return testserver.NewDBForTest(t, testserver.NonStableDbOpt()) }, 130 | }, 131 | { 132 | name: "InsecureCustomVersion", 133 | instantiation: func(t *testing.T) (*sql.DB, func()) { 134 | return testserver.NewDBForTest(t, testserver.CustomVersionOpt("v21.2.15")) 135 | }, 136 | }, 137 | { 138 | name: "InsecureWithCustomizedMemSizeNonStable", 139 | instantiation: func(t *testing.T) (*sql.DB, func()) { 140 | return testserver.NewDBForTest(t, testserver.SetStoreMemSizeOpt(0.3), testserver.NonStableDbOpt()) 141 | }, 142 | }, 143 | { 144 | name: "SecureClientCertNonStable", 145 | instantiation: func(t *testing.T) (*sql.DB, func()) { 146 | return testserver.NewDBForTest(t, testserver.SecureOpt(), testserver.NonStableDbOpt()) 147 | }, 148 | }, 149 | { 150 | name: "SecurePasswordNonStable", 151 | instantiation: func(t *testing.T) (*sql.DB, func()) { 152 | return testserver.NewDBForTest(t, testserver.SecureOpt(), 153 | testserver.RootPasswordOpt(testPW), testserver.NonStableDbOpt()) 154 | }, 155 | }, 156 | { 157 | name: "InsecureTenantStoreOnDiskNonStable", 158 | instantiation: func(t *testing.T) (*sql.DB, func()) { 159 | return testserver.NewDBForTest(t, testserver.StoreOnDiskOpt(), testserver.NonStableDbOpt()) 160 | }, 161 | }, 162 | { 163 | name: "SecureTenantStoreOnDiskNonStable", 164 | instantiation: func(t *testing.T) (*sql.DB, func()) { 165 | return testserver.NewDBForTest(t, 166 | testserver.SecureOpt(), 167 | testserver.StoreOnDiskOpt(), 168 | testserver.NonStableDbOpt(), 169 | ) 170 | }, 171 | }, 172 | { 173 | name: "SecureTenantThroughProxyNonStable", 174 | instantiation: func(t *testing.T) (*sql.DB, func()) { 175 | return newTenantDBForTest( 176 | t, 177 | true, /* secure */ 178 | true, /* proxy */ 179 | noPW, /* pw */ 180 | false, /* diskStore */ 181 | defStoreMemSize, /* storeMemSize */ 182 | true, /* nonStableDB */ 183 | ) 184 | }, 185 | }, 186 | { 187 | name: "SecureTenantThroughProxyCustomPasswordNonStable", 188 | instantiation: func(t *testing.T) (*sql.DB, func()) { 189 | return newTenantDBForTest( 190 | t, 191 | true, /* secure */ 192 | true, /* proxy */ 193 | testPW, /* pw */ 194 | false, /* diskStore */ 195 | defStoreMemSize, /* storeMemSize */ 196 | true, /* nonStableDB */ 197 | ) 198 | }, 199 | }, 200 | { 201 | name: "Insecure 3 Node", 202 | instantiation: func(t *testing.T) (*sql.DB, func()) { 203 | return testserver.NewDBForTest(t, testserver.ThreeNodeOpt(), 204 | testserver.AddListenAddrPortOpt(26257), 205 | testserver.AddListenAddrPortOpt(26258), 206 | testserver.AddListenAddrPortOpt(26259)) 207 | }, 208 | }, 209 | { 210 | name: "Insecure 3 Node On Disk", 211 | instantiation: func(t *testing.T) (*sql.DB, func()) { 212 | return testserver.NewDBForTest(t, testserver.ThreeNodeOpt(), 213 | testserver.StoreOnDiskOpt(), 214 | testserver.AddListenAddrPortOpt(26257), 215 | testserver.AddListenAddrPortOpt(26258), 216 | testserver.AddListenAddrPortOpt(26259)) 217 | }, 218 | }, 219 | { 220 | name: "Insecure 3 Node On Disk No Ports Specified", 221 | instantiation: func(t *testing.T) (*sql.DB, func()) { 222 | return testserver.NewDBForTest(t, testserver.ThreeNodeOpt(), 223 | testserver.StoreOnDiskOpt()) 224 | }, 225 | }, 226 | { 227 | name: "Insecure 3 Node With Http Ports", 228 | instantiation: func(t *testing.T) (*sql.DB, func()) { 229 | return testserver.NewDBForTest(t, testserver.ThreeNodeOpt(), 230 | testserver.StoreOnDiskOpt(), 231 | testserver.AddHttpPortOpt(8080), 232 | testserver.AddHttpPortOpt(8081), 233 | testserver.AddHttpPortOpt(8082), 234 | ) 235 | }, 236 | }, 237 | { 238 | name: "Demo mode", 239 | instantiation: func(t *testing.T) (*sql.DB, func()) { 240 | return testserver.NewDBForTest(t, 241 | testserver.DemoModeOpt(), 242 | ) 243 | }, 244 | }, 245 | { 246 | name: "Demo mode 3-node", 247 | instantiation: func(t *testing.T) (*sql.DB, func()) { 248 | return testserver.NewDBForTest(t, testserver.ThreeNodeOpt(), 249 | testserver.DemoModeOpt(), 250 | ) 251 | }, 252 | }, 253 | } { 254 | t.Run(tc.name, func(t *testing.T) { 255 | db, stop := tc.instantiation(t) 256 | defer stop() 257 | var out int 258 | row := db.QueryRow("SELECT 1") 259 | require.NoError(t, row.Scan(&out)) 260 | require.Equal(t, out, 1) 261 | _, err := db.Exec("SELECT 1") 262 | require.NoError(t, err) 263 | }) 264 | } 265 | } 266 | 267 | func TestCockroachBinaryPathOpt(t *testing.T) { 268 | crdbBinary := "doesnotexist" 269 | _, err := testserver.NewTestServer(testserver.CockroachBinaryPathOpt(crdbBinary)) 270 | if err == nil { 271 | t.Fatal("expected err, got nil") 272 | } 273 | // Confirm that the command is updated to reference the absolute path 274 | // of the custom cockroachdb binary. 275 | cmdPath, fPathErr := filepath.Abs(crdbBinary) 276 | if fPathErr != nil { 277 | cmdPath = crdbBinary 278 | } 279 | wantSubstring := fmt.Sprintf("command %s version failed", cmdPath) 280 | if msg := err.Error(); !strings.Contains(msg, wantSubstring) { 281 | t.Fatalf("error message %q does not contain %q", msg, wantSubstring) 282 | } 283 | } 284 | 285 | func TestCockroachExternalIODirOpt(t *testing.T) { 286 | externalDir, err := os.MkdirTemp("/tmp", "cockroach-testserver") 287 | require.NoError(t, err) 288 | defer func() { 289 | err := os.RemoveAll(externalDir) 290 | require.NoError(t, err) 291 | }() 292 | 293 | db, cleanup := testserver.NewDBForTest(t, testserver.ExternalIODirOpt(externalDir)) 294 | defer cleanup() 295 | 296 | // test that we can use external dir 297 | _, err = db.Exec("BACKUP INTO 'nodelocal://self/backup'") 298 | require.NoError(t, err) 299 | 300 | // test that external dir has files 301 | f, err := os.Open(externalDir) 302 | require.NoError(t, err) 303 | defer f.Close() 304 | _, err = f.Readdirnames(1) 305 | require.NoError(t, err) 306 | } 307 | 308 | func TestPGURLWhitespace(t *testing.T) { 309 | ts, err := testserver.NewTestServer() 310 | if err != nil { 311 | t.Fatal(err) 312 | } 313 | url := ts.PGURL().String() 314 | if trimmed := strings.TrimSpace(url); url != trimmed { 315 | t.Errorf("unexpected whitespace in server URL: %q", url) 316 | } 317 | } 318 | 319 | func TestSingleNodePort(t *testing.T) { 320 | port, err := getFreePort() 321 | require.NoError(t, err) 322 | 323 | ts, err := testserver.NewTestServer(testserver.AddListenAddrPortOpt(port)) 324 | if err != nil { 325 | t.Fatal(err) 326 | } 327 | defer ts.Stop() 328 | 329 | // check that port overriding worked 330 | url := ts.PGURL() 331 | require.Equal(t, strconv.Itoa(port), url.Port()) 332 | 333 | db, err := sql.Open("postgres", url.String()) 334 | require.NoError(t, err) 335 | 336 | // check that connection was successful 337 | var out int 338 | row := db.QueryRow("SELECT 1") 339 | require.NoError(t, row.Scan(&out)) 340 | require.Equal(t, 1, out) 341 | } 342 | 343 | // tenantInterface is defined in order to use tenant-related methods on the 344 | // TestServer. 345 | type tenantInterface interface { 346 | NewTenantServer(proxy bool) (testserver.TestServer, error) 347 | } 348 | 349 | // newTenantDBForTest is a testing helper function that starts a TestServer 350 | // process and a SQL tenant process pointed at this TestServer. A sql connection 351 | // to the tenant and a cleanup function are returned. 352 | func newTenantDBForTest( 353 | t *testing.T, 354 | secure bool, 355 | proxy bool, 356 | pw string, 357 | diskStore bool, 358 | storeMemSize float64, 359 | nonStableDB bool, 360 | ) (*sql.DB, func()) { 361 | t.Helper() 362 | var opts []testserver.TestServerOpt 363 | if secure { 364 | opts = append(opts, testserver.SecureOpt()) 365 | } 366 | if diskStore { 367 | opts = append(opts, testserver.StoreOnDiskOpt()) 368 | } 369 | if pw != "" { 370 | opts = append(opts, testserver.RootPasswordOpt(pw)) 371 | } 372 | if storeMemSize >= 0 { 373 | opts = append(opts, testserver.SetStoreMemSizeOpt(storeMemSize)) 374 | } else { 375 | t.Fatal("Percentage memory size for data storage cannot be nagative") 376 | } 377 | if nonStableDB { 378 | opts = append(opts, testserver.NonStableDbOpt()) 379 | } 380 | ts, err := testserver.NewTestServer(opts...) 381 | if err != nil { 382 | t.Fatal(err) 383 | } 384 | tenant, err := ts.(tenantInterface).NewTenantServer(proxy) 385 | if err != nil { 386 | t.Fatal(err) 387 | } 388 | url := tenant.PGURL() 389 | if url == nil { 390 | t.Fatal("postgres url not found") 391 | } 392 | db, err := sql.Open("postgres", url.String()) 393 | if err != nil { 394 | t.Fatal(err) 395 | } 396 | return db, func() { 397 | _ = db.Close() 398 | tenant.Stop() 399 | ts.Stop() 400 | } 401 | } 402 | 403 | func TestTenant(t *testing.T) { 404 | db, stop := newTenantDBForTest( 405 | t, 406 | false, /* secure */ 407 | false, /* proxy */ 408 | noPW, /* pw */ 409 | false, /* diskStore */ 410 | defStoreMemSize, /* storeMemSize */ 411 | false, /* nonStableDB */ 412 | ) 413 | 414 | defer stop() 415 | if _, err := db.Exec("SELECT 1"); err != nil { 416 | t.Fatal(err) 417 | } 418 | 419 | if _, err := db.Exec("SELECT crdb_internal.create_tenant(123)"); err == nil { 420 | t.Fatal("expected an error when creating a tenant since secondary tenants should not be able to do so") 421 | } 422 | } 423 | 424 | func TestFlockOnDownloadedCRDB(t *testing.T) { 425 | for _, tc := range []struct { 426 | name string 427 | instantiation func(*testing.T) (*sql.DB, func()) 428 | }{ 429 | { 430 | // It will print "waiting for download of ..." in log for about 30 seconds. 431 | name: "DownloadPassed", 432 | instantiation: func(t *testing.T) (*sql.DB, func()) { 433 | return testFlockWithDownloadPassing(t) 434 | }, 435 | }, { 436 | name: "DownloadKilled", 437 | instantiation: func(t *testing.T) (*sql.DB, func()) { 438 | return testFlockWithDownloadKilled(t) 439 | }, 440 | }, 441 | } { 442 | t.Run(tc.name, func(t *testing.T) { 443 | db, stop := tc.instantiation(t) 444 | defer stop() 445 | if _, err := db.Exec("SELECT 1"); err != nil { 446 | t.Fatal(err) 447 | } 448 | }) 449 | } 450 | } 451 | 452 | func getFreePort() (int, error) { 453 | addr, err := net.ResolveTCPAddr("tcp", ":0") 454 | if err != nil { 455 | return 0, err 456 | } 457 | 458 | l, err := net.ListenTCP("tcp", addr) 459 | if err != nil { 460 | return 0, err 461 | } 462 | port := l.Addr().(*net.TCPAddr).Port 463 | if err != nil { 464 | return 0, err 465 | } 466 | 467 | err = l.Close() 468 | if err != nil { 469 | return 0, err 470 | } 471 | 472 | return port, err 473 | } 474 | 475 | func TestRestartNodeParallel(t *testing.T) { 476 | require.NoError(t, os.Mkdir("./temp_binaries", 0755)) 477 | defer func() { 478 | require.NoError(t, os.RemoveAll("./temp_binaries")) 479 | }() 480 | var fileName string 481 | switch runtime.GOOS { 482 | case "darwin": 483 | fileName = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", "v22.1.6") 484 | require.NoError(t, downloadBinaryTest( 485 | fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 486 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 487 | case "linux": 488 | fileName = fmt.Sprintf("cockroach-%s.linux-amd64", "v22.1.6") 489 | require.NoError(t, downloadBinaryTest( 490 | fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 491 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 492 | default: 493 | t.Fatalf("unsupported os for test: %s", runtime.GOOS) 494 | } 495 | 496 | tarCmd := exec.Command("tar", "-zxvf", fmt.Sprintf("./temp_binaries/%s.tgz", fileName), "-C", "./temp_binaries") 497 | require.NoError(t, tarCmd.Start()) 498 | require.NoError(t, tarCmd.Wait()) 499 | 500 | mu := sync.Mutex{} 501 | usedPorts := make(map[int]struct{}) 502 | const ParallelExecs = 5 503 | var wg sync.WaitGroup 504 | wg.Add(ParallelExecs) 505 | for i := 0; i < ParallelExecs; i++ { 506 | go func() { 507 | ports := make([]int, 3) 508 | for j := 0; j < 3; j++ { 509 | for { 510 | port, err := getFreePort() 511 | require.NoError(t, err) 512 | mu.Lock() 513 | _, found := usedPorts[port] 514 | if !found { 515 | usedPorts[port] = struct{}{} 516 | } 517 | ports[j] = port 518 | mu.Unlock() 519 | if !found { 520 | break 521 | } 522 | } 523 | } 524 | testRestartNode(t, ports, "temp_binaries/"+fileName+"/cockroach") 525 | wg.Done() 526 | }() 527 | } 528 | wg.Wait() 529 | } 530 | 531 | func testRestartNode(t *testing.T, ports []int, binaryPath string) { 532 | const pollListenURLTimeout = 150 533 | ts, err := testserver.NewTestServer( 534 | testserver.ThreeNodeOpt(), 535 | testserver.StoreOnDiskOpt(), 536 | testserver.AddListenAddrPortOpt(ports[0]), 537 | testserver.AddListenAddrPortOpt(ports[1]), 538 | testserver.AddListenAddrPortOpt(ports[2]), 539 | testserver.CockroachBinaryPathOpt(binaryPath), 540 | testserver.PollListenURLTimeoutOpt(pollListenURLTimeout)) 541 | require.NoError(t, err) 542 | defer ts.Stop() 543 | for i := 0; i < 3; i++ { 544 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 545 | } 546 | 547 | log.Printf("Stopping Node 0") 548 | require.NoError(t, ts.StopNode(0)) 549 | for i := 1; i < 3; i++ { 550 | url := ts.PGURLForNode(i) 551 | 552 | db, err := sql.Open("postgres", url.String()) 553 | require.NoError(t, err) 554 | var out int 555 | row := db.QueryRow("SELECT 1") 556 | err = row.Scan(&out) 557 | require.NoError(t, err) 558 | require.NoError(t, db.Close()) 559 | } 560 | 561 | require.NoError(t, ts.StartNode(0)) 562 | require.NoError(t, ts.WaitForInitFinishForNode(0)) 563 | 564 | for i := 0; i < 3; i++ { 565 | url := ts.PGURLForNode(i) 566 | db, err := sql.Open("postgres", url.String()) 567 | require.NoError(t, err) 568 | 569 | var out int 570 | row := db.QueryRow("SELECT 1") 571 | err = row.Scan(&out) 572 | require.NoError(t, err) 573 | require.NoError(t, db.Close()) 574 | } 575 | 576 | url := ts.PGURLForNode(0) 577 | db, err := sql.Open("postgres", url.String()) 578 | require.NoError(t, err) 579 | var out int 580 | row := db.QueryRow("SELECT 1") 581 | err = row.Scan(&out) 582 | require.NoError(t, err) 583 | require.NoError(t, db.Close()) 584 | } 585 | 586 | func downloadBinaryTest(filepath string, url string) error { 587 | // Get the data 588 | resp, err := http.Get(url) 589 | if err != nil { 590 | return err 591 | } 592 | defer resp.Body.Close() 593 | 594 | // Create the file 595 | out, err := os.Create(filepath) 596 | if err != nil { 597 | return err 598 | } 599 | defer out.Close() 600 | 601 | // Write the body to file 602 | _, err = io.Copy(out, resp.Body) 603 | return err 604 | } 605 | 606 | func TestUpgradeNode(t *testing.T) { 607 | oldVersion := "v21.2.12" 608 | newVersion := "v22.1.6" 609 | 610 | var oldVersionBinary, newVersionBinary string 611 | switch runtime.GOOS { 612 | case "darwin": 613 | oldVersionBinary = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", oldVersion) 614 | newVersionBinary = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", newVersion) 615 | case "linux": 616 | oldVersionBinary = fmt.Sprintf("cockroach-%s.linux-amd64", oldVersion) 617 | newVersionBinary = fmt.Sprintf("cockroach-%s.linux-amd64", newVersion) 618 | default: 619 | t.Fatalf("unsupported os for test: %s", runtime.GOOS) 620 | } 621 | 622 | getBinary := func(fileName string) { 623 | require.NoError(t, exec.Command("mkdir", "./temp_binaries").Start()) 624 | require.NoError(t, downloadBinaryTest(fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 625 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 626 | tarCmd := exec.Command("tar", "-zxvf", fmt.Sprintf("./temp_binaries/%s.tgz", fileName), "-C", "./temp_binaries") 627 | require.NoError(t, tarCmd.Start()) 628 | require.NoError(t, tarCmd.Wait()) 629 | } 630 | 631 | defer func() { 632 | require.NoError(t, exec.Command("rm", "-rf", "./temp_binaries").Run()) 633 | }() 634 | 635 | getBinary(oldVersionBinary) 636 | getBinary(newVersionBinary) 637 | 638 | absPathOldBinary, err := filepath.Abs(fmt.Sprintf("./temp_binaries/%s/cockroach", oldVersionBinary)) 639 | require.NoError(t, err) 640 | 641 | absPathNewBinary, err := filepath.Abs(fmt.Sprintf("./temp_binaries/%s/cockroach", newVersionBinary)) 642 | require.NoError(t, err) 643 | 644 | ts, err := testserver.NewTestServer( 645 | testserver.ThreeNodeOpt(), 646 | testserver.CockroachBinaryPathOpt(absPathOldBinary), 647 | testserver.UpgradeCockroachBinaryPathOpt(absPathNewBinary), 648 | testserver.StoreOnDiskOpt(), 649 | ) 650 | require.NoError(t, err) 651 | defer ts.Stop() 652 | 653 | for i := 0; i < 3; i++ { 654 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 655 | } 656 | 657 | url := ts.PGURL() 658 | db, err := sql.Open("postgres", url.String()) 659 | require.NoError(t, err) 660 | defer func() { 661 | require.NoError(t, db.Close()) 662 | }() 663 | 664 | var version string 665 | row := db.QueryRow("SHOW CLUSTER SETTING version") 666 | err = row.Scan(&version) 667 | require.NoError(t, err) 668 | 669 | _, err = db.Exec("SET CLUSTER SETTING cluster.preserve_downgrade_option = '21.2';") 670 | require.NoError(t, err) 671 | require.NoError(t, db.Close()) 672 | 673 | for i := 0; i < 3; i++ { 674 | require.NoError(t, ts.UpgradeNode(i)) 675 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 676 | } 677 | 678 | for i := 0; i < 3; i++ { 679 | url := ts.PGURLForNode(i) 680 | 681 | db, err = sql.Open("postgres", url.String()) 682 | require.NoError(t, err) 683 | 684 | var out int 685 | row = db.QueryRow("SELECT 1") 686 | err = row.Scan(&out) 687 | require.NoError(t, err) 688 | require.NoError(t, db.Close()) 689 | } 690 | 691 | db, err = sql.Open("postgres", ts.PGURL().String()) 692 | require.NoError(t, err) 693 | defer db.Close() 694 | _, err = db.Exec("RESET CLUSTER SETTING cluster.preserve_downgrade_option;") 695 | require.NoError(t, err) 696 | 697 | updated := false 698 | for start := time.Now(); time.Since(start) < 300*time.Second; { 699 | row = db.QueryRow("SHOW CLUSTER SETTING version") 700 | err = row.Scan(&version) 701 | if err != nil { 702 | t.Fatal(err) 703 | } 704 | if version == "22.1" { 705 | updated = true 706 | break 707 | } 708 | time.Sleep(time.Second) 709 | } 710 | 711 | if !updated { 712 | t.Fatal("update to 22.1 did not complete") 713 | } 714 | } 715 | 716 | // testFlockWithDownloadPassing is to test the flock over downloaded CRDB binary with 717 | // two goroutines, the second goroutine waits for the first goroutine to 718 | // finish downloading the CRDB binary into a local file. 719 | func testFlockWithDownloadPassing( 720 | t *testing.T, opts ...testserver.TestServerOpt, 721 | ) (*sql.DB, func()) { 722 | var wg = sync.WaitGroup{} 723 | 724 | localFile, err := getLocalFile(false) 725 | if err != nil { 726 | t.Fatal(err) 727 | } 728 | 729 | // Remove existing local file, to ensure that the first goroutine will download the CRDB binary. 730 | if err := removeExistingLocalFile(localFile); err != nil { 731 | t.Fatal(err) 732 | } 733 | 734 | wg.Add(2) 735 | var db *sql.DB 736 | var stop func() 737 | 738 | // First NewDBForTest goroutine to download the CRDB binary to the local file. 739 | go func() { 740 | db, stop = testserver.NewDBForTest(t, opts...) 741 | wg.Done() 742 | }() 743 | // Wait for 2 seconds, and then start the second NewDBForTest process. 744 | time.Sleep(2000 * time.Millisecond) 745 | // The second goroutine needs to wait until the first goroutine finishes downloading. 746 | // It will print "waiting for download of ..." in log for about 30 seconds. 747 | go func() { 748 | db, stop = testserver.NewDBForTest(t, opts...) 749 | wg.Done() 750 | }() 751 | wg.Wait() 752 | return db, stop 753 | } 754 | 755 | // testFlockWithDownloadKilled is to simulate the case that a NewDBForTest process is 756 | // killed before finishing downloading CRDB binary, and another NewDBForTest process has 757 | // to remove the existing local file and redownload. 758 | func testFlockWithDownloadKilled(t *testing.T) (*sql.DB, func()) { 759 | localFile, err := getLocalFile(false) 760 | if err != nil { 761 | t.Fatal(err) 762 | } 763 | 764 | // Remove existing local file, to ensure that the first goroutine will download the CRDB binary. 765 | if err := removeExistingLocalFile(localFile); err != nil { 766 | t.Fatal(err) 767 | } 768 | 769 | // First NewDBForTest process to download the CRDB binary to the local file, 770 | // but killed in the middle. 771 | _, stop := testserver.NewDBForTest(t, testserver.StopDownloadInMiddleOpt()) 772 | stop() 773 | // Start the second NewDBForTest process. 774 | // It will remove the local file and redownload. 775 | return testserver.NewDBForTest(t) 776 | 777 | } 778 | 779 | // getLocalFile returns the to-be-downloaded CRDB binary's local path. 780 | func getLocalFile(nonStable bool) (string, error) { 781 | _, latestStableVersion, err := testserver.GetDownloadURL("", nonStable) 782 | if err != nil { 783 | return "", err 784 | } 785 | filename, err := testserver.GetDownloadFilename(latestStableVersion) 786 | if err != nil { 787 | return "", err 788 | } 789 | localFile := filepath.Join(os.TempDir(), filename) 790 | return localFile, err 791 | } 792 | 793 | // removeExistingLocalFile removes the existing local file for downloaded CRDB binary. 794 | func removeExistingLocalFile(localFile string) error { 795 | _, err := os.Stat(localFile) 796 | if err == nil { 797 | if err := os.Remove(localFile); err != nil { 798 | return fmt.Errorf("cannot remove local file: %s", err) 799 | } 800 | } 801 | return nil 802 | } 803 | 804 | func TestLocalityFlagsOpt(t *testing.T) { 805 | ts, err := testserver.NewTestServer( 806 | testserver.ThreeNodeOpt(), 807 | testserver.LocalityFlagsOpt("region=us-east1", "region=us-central1", "region=us-west1")) 808 | require.NoError(t, err) 809 | 810 | for i := 0; i < 3; i++ { 811 | ts.WaitForInitFinishForNode(i) 812 | } 813 | 814 | db, err := sql.Open("postgres", ts.PGURL().String()) 815 | require.NoError(t, err) 816 | 817 | found := map[string]bool{} 818 | 819 | rows, err := db.Query("SELECT region FROM [SHOW REGIONS]") 820 | require.NoError(t, err) 821 | defer rows.Close() 822 | for rows.Next() { 823 | var region string 824 | require.NoError(t, rows.Scan(®ion)) 825 | found[region] = true 826 | } 827 | 828 | require.Equal(t, map[string]bool{ 829 | "us-east1": true, 830 | "us-central1": true, 831 | "us-west1": true, 832 | }, found) 833 | } 834 | 835 | func TestCockroachLogsDirOpt(t *testing.T) { 836 | logsDir, err := os.MkdirTemp("", "logs-dir-opt") 837 | require.NoError(t, err) 838 | defer require.NoError(t, os.RemoveAll(logsDir)) 839 | 840 | ts, err := testserver.NewTestServer( 841 | testserver.ThreeNodeOpt(), 842 | testserver.CockroachLogsDirOpt(logsDir)) 843 | require.NoError(t, err) 844 | 845 | for i := 0; i < 3; i++ { 846 | if err := ts.WaitForInitFinishForNode(i); err != nil { 847 | // Make sure we stop the testserver in this case as well. 848 | ts.Stop() 849 | require.NoError(t, err) 850 | } 851 | } 852 | 853 | // This should delete all resources, but log files should 854 | // continue to exist under `logsDir`. 855 | ts.Stop() 856 | 857 | for _, nodeID := range []string{"0", "1", "2"} { 858 | for _, logFile := range []string{"cockroach.stdout", "cockroach.stderr"} { 859 | _, err := os.Stat(filepath.Join(logsDir, nodeID, logFile)) 860 | require.NoError(t, err) 861 | } 862 | } 863 | } 864 | --------------------------------------------------------------------------------