├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── crdb ├── README.md ├── common.go ├── crdbgorm │ ├── gorm.go │ └── gorm_test.go ├── crdbpgx │ ├── README.md │ ├── pgx.go │ └── pgx_test.go ├── crdbpgxv5 │ ├── README.md │ ├── pgx.go │ └── pgx_test.go ├── crdbsqlx │ ├── sqlx.go │ └── sqlx_test.go ├── error.go ├── error_test.go ├── testing_util.go ├── tx.go └── tx_test.go ├── go.mod ├── go.sum └── testserver ├── README.md ├── binaries.go ├── tenant.go ├── testserver.go ├── testserver_test.go ├── testservernode.go └── version └── version.go /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | *.test 4 | testserver/cockroach-data/ 5 | testserver/temp_binaries/ 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Note that unfortunately there is no generic way of extracting a pg error code; 8 | the library has to recognize driver-dependent error types. We currently use 9 | the `SQLState() string` method that is implemented in both 10 | [`github.com/lib/pq`](https://github.com/lib/pq), since version 1.10.6, and 11 | [`github.com/jackc/pgx`](https://github.com/jackc/pgx) when used in database/sql 12 | driver mode. 13 | 14 | Subpackages provide support for [gorm](https://github.com/go-gorm/gorm), [pgx](https://github.com/jackc/pgx), and [sqlx](https://github.com/jmoiron/sqlx) used in standalone-library mode. 15 | 16 | Note for developers: if you make any changes here (especially if they modify public 17 | APIs), please verify that the code in https://github.com/cockroachdb/examples-go 18 | still works and update as necessary. 19 | -------------------------------------------------------------------------------- /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 "context" 18 | 19 | // Tx abstracts the operations needed by ExecuteInTx so that different 20 | // frameworks (e.g. go's sql package, pgx, gorm) can be used with ExecuteInTx. 21 | type Tx interface { 22 | Exec(context.Context, string, ...interface{}) error 23 | Commit(context.Context) error 24 | Rollback(context.Context) error 25 | } 26 | 27 | // ExecuteInTx runs fn inside tx. This method is primarily intended for internal 28 | // use. See other packages for higher-level, framework-specific ExecuteTx() 29 | // functions. 30 | // 31 | // *WARNING*: It is assumed that no statements have been executed on the 32 | // supplied Tx. ExecuteInTx will only retry statements that are performed within 33 | // the supplied closure (fn). Any statements performed on the tx before 34 | // ExecuteInTx is invoked will *not* be re-run if the transaction needs to be 35 | // retried. 36 | // 37 | // fn is subject to the same restrictions as the fn passed to ExecuteTx. 38 | func ExecuteInTx(ctx context.Context, tx Tx, fn func() error) (err error) { 39 | defer func() { 40 | r := recover() 41 | 42 | if r == nil && err == nil { 43 | // Ignore commit errors. The tx has already been committed by RELEASE. 44 | _ = tx.Commit(ctx) 45 | return 46 | } 47 | 48 | // We always need to execute a Rollback() so sql.DB releases the 49 | // connection. 50 | _ = tx.Rollback(ctx) 51 | 52 | if r != nil { 53 | panic(r) 54 | } 55 | }() 56 | 57 | // Specify that we intend to retry this txn in case of CockroachDB retryable 58 | // errors. 59 | if err = tx.Exec(ctx, "SAVEPOINT cockroach_restart"); err != nil { 60 | return err 61 | } 62 | 63 | maxRetries := numRetriesFromContext(ctx) 64 | retryCount := 0 65 | for { 66 | releaseFailed := false 67 | err = fn() 68 | if err == nil { 69 | // RELEASE acts like COMMIT in CockroachDB. We use it since it gives us an 70 | // opportunity to react to retryable errors, whereas tx.Commit() doesn't. 71 | if err = tx.Exec(ctx, "RELEASE SAVEPOINT cockroach_restart"); err == nil { 72 | return nil 73 | } 74 | releaseFailed = true 75 | } 76 | 77 | // We got an error; let's see if it's a retryable one and, if so, restart. 78 | if !errIsRetryable(err) { 79 | if releaseFailed { 80 | err = newAmbiguousCommitError(err) 81 | } 82 | return err 83 | } 84 | 85 | if rollbackErr := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT cockroach_restart"); rollbackErr != nil { 86 | return newTxnRestartError(rollbackErr, err) 87 | } 88 | 89 | retryCount++ 90 | if maxRetries > 0 && retryCount > maxRetries { 91 | return newMaxRetriesExceededError(err, maxRetries) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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) *TxnRestartError { 70 | const msgPattern = "restarting txn failed. ROLLBACK TO SAVEPOINT " + 71 | "encountered error: %s. Original error: %s." 72 | return &TxnRestartError{ 73 | txError: txError{cause: err}, 74 | retryCause: retryErr, 75 | msg: fmt.Sprintf(msgPattern, 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/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 | -------------------------------------------------------------------------------- /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/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 | ) 24 | 25 | // Execute runs fn and retries it as needed. It is used to add retry handling to 26 | // the execution of a single statement. If a multi-statement transaction is 27 | // being run, use ExecuteTx instead. 28 | // 29 | // Retry handling for individual statements (implicit transactions) is usually 30 | // performed automatically on the CockroachDB SQL gateway. As such, use of this 31 | // function is generally not necessary. The exception to this rule is that 32 | // automatic retries for individual statements are disabled once CockroachDB 33 | // begins streaming results for the statements back to the client. By default, 34 | // result streaming does not begin until the size of the result being produced 35 | // for the client, including protocol overhead, exceeds 16KiB. As long as the 36 | // results of a single statement or batch of statements are known to stay clear 37 | // of this limit, the client does not need to worry about retries and should not 38 | // need to use this function. 39 | // 40 | // For more information about automatic transaction retries in CockroachDB, see 41 | // https://cockroachlabs.com/docs/stable/transactions.html#automatic-retries. 42 | // 43 | // NOTE: the supplied fn closure should not have external side effects beyond 44 | // changes to the database. 45 | // 46 | // fn must take care when wrapping errors returned from the database driver with 47 | // additional context. For example, if the SELECT statement fails in the 48 | // following snippet, the original retryable error will be masked by the call to 49 | // fmt.Errorf, and the transaction will not be automatically retried. 50 | // 51 | // crdb.Execute(func () error { 52 | // rows, err := db.QueryContext(ctx, "SELECT ...") 53 | // if err != nil { 54 | // return fmt.Errorf("scanning row: %s", err) 55 | // } 56 | // defer rows.Close() 57 | // for rows.Next() { 58 | // // ... 59 | // } 60 | // if err := rows.Err(); err != nil { 61 | // return fmt.Errorf("scanning row: %s", err) 62 | // } 63 | // return nil 64 | // }) 65 | // 66 | // Instead, add context by returning an error that implements either: 67 | // - a `Cause() error` method, in the manner of github.com/pkg/errors, or 68 | // - an `Unwrap() error` method, in the manner of the Go 1.13 standard 69 | // library. 70 | // 71 | // To achieve this, you can implement your own error type, or use 72 | // `errors.Wrap()` from github.com/pkg/errors or 73 | // github.com/cockroachdb/errors or a similar package, or use go 74 | // 1.13's special `%w` formatter with fmt.Errorf(), for example 75 | // fmt.Errorf("scanning row: %w", err). 76 | // 77 | // import "github.com/pkg/errors" 78 | // 79 | // crdb.Execute(func () error { 80 | // rows, err := db.QueryContext(ctx, "SELECT ...") 81 | // if err != nil { 82 | // return errors.Wrap(err, "scanning row") 83 | // } 84 | // defer rows.Close() 85 | // for rows.Next() { 86 | // // ... 87 | // } 88 | // if err := rows.Err(); err != nil { 89 | // return errors.Wrap(err, "scanning row") 90 | // } 91 | // return nil 92 | // }) 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 maximum retry count 109 | // obtained from the context. It is used to add configurable retry handling to 110 | // the execution of a single statement. If a multi-statement transaction is 111 | // being run, use ExecuteTx instead. 112 | // 113 | // The maximum number of retries can be configured using WithMaxRetries(ctx, n). 114 | // Setting n=0 allows one attempt with no retries. If the number of retries is 115 | // exhausted and the last attempt resulted in a retryable error, ExecuteCtx 116 | // returns a max retries exceeded error wrapping the last retryable error 117 | // encountered. 118 | // 119 | // The fn parameter accepts variadic arguments which are passed through on each 120 | // retry attempt, allowing for flexible parameterization of the retried operation. 121 | // 122 | // As with Execute, retry handling for individual statements (implicit transactions) 123 | // is usually performed automatically on the CockroachDB SQL gateway, making use 124 | // of this function generally unnecessary. However, automatic retries are disabled 125 | // once result streaming begins (typically when results exceed 16KiB). 126 | // 127 | // NOTE: the supplied fn closure should not have external side effects beyond 128 | // changes to the database. 129 | // 130 | // fn must take care when wrapping errors returned from the database driver with 131 | // additional context. To preserve retry behavior, errors should implement either 132 | // `Cause() error` (github.com/pkg/errors) or `Unwrap() error` (Go 1.13+). 133 | // For example: 134 | // 135 | // crdb.ExecuteCtx(ctx, func(ctx context.Context, args ...interface{}) error { 136 | // id := args[0].(int) 137 | // rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id) 138 | // if err != nil { 139 | // return fmt.Errorf("scanning row: %w", err) // uses %w for proper error wrapping 140 | // } 141 | // defer rows.Close() 142 | // // ... 143 | // return nil 144 | // }, userID) 145 | func ExecuteCtx(ctx context.Context, fn ExecuteCtxFunc, args ...interface{}) (err error) { 146 | maxRetries := numRetriesFromContext(ctx) 147 | for n := 0; n <= maxRetries; n++ { 148 | if err = ctx.Err(); err != nil { 149 | return err 150 | } 151 | 152 | err = fn(ctx, args...) 153 | if err == nil || !errIsRetryable(err) { 154 | return err 155 | } 156 | } 157 | 158 | return newMaxRetriesExceededError(err, maxRetries) 159 | } 160 | 161 | type txConfigKey struct{} 162 | 163 | // WithMaxRetries configures context so that ExecuteTx retries tx specified 164 | // number of times when encountering retryable errors. 165 | // Setting retries to 0 will retry indefinitely. 166 | func WithMaxRetries(ctx context.Context, retries int) context.Context { 167 | return context.WithValue(ctx, txConfigKey{}, retries) 168 | } 169 | 170 | const defaultRetries = 50 171 | 172 | func numRetriesFromContext(ctx context.Context) int { 173 | if v := ctx.Value(txConfigKey{}); v != nil { 174 | if retries, ok := v.(int); ok && retries >= 0 { 175 | return retries 176 | } 177 | } 178 | return defaultRetries 179 | } 180 | 181 | // ExecuteTx runs fn inside a transaction and retries it as needed. On 182 | // non-retryable failures, the transaction is aborted and rolled back; on 183 | // success, the transaction is committed. 184 | // 185 | // There are cases where the state of a transaction is inherently ambiguous: if 186 | // we err on RELEASE with a communication error it's unclear if the transaction 187 | // has been committed or not (similar to erroring on COMMIT in other databases). 188 | // In that case, we return AmbiguousCommitError. 189 | // 190 | // There are cases when restarting a transaction fails: we err on ROLLBACK to 191 | // the SAVEPOINT. In that case, we return a TxnRestartError. 192 | // 193 | // For more information about CockroachDB's transaction model, see 194 | // https://cockroachlabs.com/docs/stable/transactions.html. 195 | // 196 | // NOTE: the supplied fn closure should not have external side effects beyond 197 | // changes to the database. 198 | // 199 | // fn must take care when wrapping errors returned from the database driver with 200 | // additional context. For example, if the UPDATE statement fails in the 201 | // following snippet, the original retryable error will be masked by the call to 202 | // fmt.Errorf, and the transaction will not be automatically retried. 203 | // 204 | // crdb.ExecuteTx(ctx, db, txopts, func (tx *sql.Tx) error { 205 | // if err := tx.ExecContext(ctx, "UPDATE..."); err != nil { 206 | // return fmt.Errorf("updating record: %s", err) 207 | // } 208 | // return nil 209 | // }) 210 | // 211 | // Instead, add context by returning an error that implements either: 212 | // - a `Cause() error` method, in the manner of github.com/pkg/errors, or 213 | // - an `Unwrap() error` method, in the manner of the Go 1.13 standard 214 | // library. 215 | // 216 | // To achieve this, you can implement your own error type, or use 217 | // `errors.Wrap()` from github.com/pkg/errors or 218 | // github.com/cockroachdb/errors or a similar package, or use go 219 | // 1.13's special `%w` formatter with fmt.Errorf(), for example 220 | // fmt.Errorf("scanning row: %w", err). 221 | // 222 | // import "github.com/pkg/errors" 223 | // 224 | // crdb.ExecuteTx(ctx, db, txopts, func (tx *sql.Tx) error { 225 | // if err := tx.ExecContext(ctx, "UPDATE..."); err != nil { 226 | // return errors.Wrap(err, "updating record") 227 | // } 228 | // return nil 229 | // }) 230 | // 231 | func ExecuteTx(ctx context.Context, db *sql.DB, opts *sql.TxOptions, fn func(*sql.Tx) error) error { 232 | // Start a transaction. 233 | tx, err := db.BeginTx(ctx, opts) 234 | if err != nil { 235 | return err 236 | } 237 | return ExecuteInTx(ctx, stdlibTxnAdapter{tx}, func() error { return fn(tx) }) 238 | } 239 | 240 | type stdlibTxnAdapter struct { 241 | tx *sql.Tx 242 | } 243 | 244 | var _ Tx = stdlibTxnAdapter{} 245 | 246 | // Exec is part of the tx interface. 247 | func (tx stdlibTxnAdapter) Exec(ctx context.Context, q string, args ...interface{}) error { 248 | _, err := tx.tx.ExecContext(ctx, q, args...) 249 | return err 250 | } 251 | 252 | // Commit is part of the tx interface. 253 | func (tx stdlibTxnAdapter) Commit(context.Context) error { 254 | return tx.tx.Commit() 255 | } 256 | 257 | // Commit is part of the tx interface. 258 | func (tx stdlibTxnAdapter) Rollback(context.Context) error { 259 | return tx.tx.Rollback() 260 | } 261 | 262 | func errIsRetryable(err error) bool { 263 | // We look for either: 264 | // - the standard PG errcode SerializationFailureError:40001 or 265 | // - the Cockroach extension errcode RetriableError:CR000. This extension 266 | // has been removed server-side, but support for it has been left here for 267 | // now to maintain backwards compatibility. 268 | code := errCode(err) 269 | return code == "CR000" || code == "40001" 270 | } 271 | 272 | func errCode(err error) string { 273 | var sqlErr errWithSQLState 274 | if errors.As(err, &sqlErr) { 275 | return sqlErr.SQLState() 276 | } 277 | 278 | return "" 279 | } 280 | 281 | // errWithSQLState is implemented by pgx (pgconn.PgError) and lib/pq 282 | type errWithSQLState interface { 283 | SQLState() string 284 | } 285 | -------------------------------------------------------------------------------- /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 | ctx := context.Background() 105 | if numRetriesFromContext(ctx) != defaultRetries { 106 | t.Fatal("expect default number of retries") 107 | } 108 | ctx = WithMaxRetries(context.Background(), 123+defaultRetries) 109 | if numRetriesFromContext(ctx) != defaultRetries+123 { 110 | t.Fatal("expected default+123 retires") 111 | } 112 | } 113 | 114 | type stdlibWriteSkewTest struct { 115 | db *sql.DB 116 | } 117 | 118 | var _ WriteSkewTest = stdlibWriteSkewTest{} 119 | 120 | func (t stdlibWriteSkewTest) Init(ctx context.Context) error { 121 | initStmt := ` 122 | CREATE DATABASE d; 123 | CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT); 124 | INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100); 125 | ` 126 | _, err := t.db.ExecContext(ctx, initStmt) 127 | return err 128 | } 129 | 130 | func (t stdlibWriteSkewTest) ExecuteTx(ctx context.Context, fn func(tx interface{}) error) error { 131 | return ExecuteTx(ctx, t.db, nil /* opts */, func(tx *sql.Tx) error { 132 | return fn(tx) 133 | }) 134 | } 135 | 136 | func (t stdlibWriteSkewTest) GetBalances(ctx context.Context, txi interface{}) (int, int, error) { 137 | tx := txi.(*sql.Tx) 138 | var rows *sql.Rows 139 | rows, err := tx.QueryContext(ctx, `SELECT balance FROM d.t WHERE acct IN (1, 2);`) 140 | if err != nil { 141 | return 0, 0, err 142 | } 143 | defer rows.Close() 144 | var bal1, bal2 int 145 | balances := []*int{&bal1, &bal2} 146 | i := 0 147 | for ; rows.Next(); i++ { 148 | if err = rows.Scan(balances[i]); err != nil { 149 | return 0, 0, err 150 | } 151 | } 152 | if i != 2 { 153 | return 0, 0, fmt.Errorf("expected two balances; got %d", i) 154 | } 155 | return bal1, bal2, nil 156 | } 157 | 158 | func (t stdlibWriteSkewTest) UpdateBalance( 159 | ctx context.Context, txi interface{}, acct, delta int, 160 | ) error { 161 | tx := txi.(*sql.Tx) 162 | _, err := tx.ExecContext(ctx, `UPDATE d.t SET balance=balance+$1 WHERE acct=$2;`, delta, acct) 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | "os/exec" 30 | "path" 31 | "path/filepath" 32 | "regexp" 33 | "runtime" 34 | "strings" 35 | "time" 36 | 37 | "github.com/cockroachdb/cockroach-go/v2/testserver/version" 38 | "github.com/gofrs/flock" 39 | "gopkg.in/yaml.v3" 40 | ) 41 | 42 | const ( 43 | latestSuffix = "LATEST" 44 | finishedFileMode = 0555 45 | writingFileMode = 0600 // Allow reads so that another process can check if there's a flock. 46 | ) 47 | 48 | const ( 49 | linuxUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.linux-%s.tgz" 50 | macUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.darwin-%s-%s.tgz" 51 | winUrlpat = "https://binaries.cockroachdb.com/cockroach-%s.windows-6.2-amd64.zip" 52 | ) 53 | 54 | // releaseDataURL is the location of the YAML file maintained by the 55 | // docs team where release information is encoded. This data is used 56 | // to render the public CockroachDB releases page. We leverage the 57 | // data in structured format to generate release information used 58 | // for testing purposes. 59 | const releaseDataURL = "https://raw.githubusercontent.com/cockroachdb/docs/main/src/current/_data/releases.yml" 60 | 61 | var muslRE = regexp.MustCompile(`(?i)\bmusl\b`) 62 | 63 | // GetDownloadURL returns the URL of a CRDB download. It creates the URL for 64 | // downloading a CRDB binary for current runtime OS. If desiredVersion is 65 | // specified, it will return the URL of the specified version. Otherwise, it 66 | // will return the URL of the latest stable cockroach binary. If nonStable is 67 | // true, the latest cockroach binary will be used. 68 | func GetDownloadURL(desiredVersion string, nonStable bool) (string, string, error) { 69 | goos := runtime.GOOS 70 | if goos == "linux" { 71 | goos += func() string { 72 | // Detect which C library is present on the system. See 73 | // https://unix.stackexchange.com/a/120381. 74 | cmd := exec.Command("ldd", "--version") 75 | out, err := cmd.Output() 76 | if err != nil { 77 | log.Printf("%s: %s: out=%q err=%v", testserverMessagePrefix, cmd.Args, out, err) 78 | } else if muslRE.Match(out) { 79 | return "-musl" 80 | } 81 | return "-gnu" 82 | }() 83 | } 84 | binaryName := fmt.Sprintf("cockroach.%s-%s", goos, runtime.GOARCH) 85 | if runtime.GOOS == "windows" { 86 | binaryName += ".exe" 87 | } 88 | 89 | var dbUrl string 90 | var err error 91 | 92 | if desiredVersion != "" { 93 | dbUrl = getDownloadUrlForVersion(desiredVersion) 94 | } else if nonStable { 95 | // For the latest (beta) CRDB, we use the `edge-binaries.cockroachdb.com` host. 96 | u := &url.URL{ 97 | Scheme: "https", 98 | Host: "edge-binaries.cockroachdb.com", 99 | Path: path.Join("cockroach", fmt.Sprintf("%s.%s", binaryName, latestSuffix)), 100 | } 101 | dbUrl = u.String() 102 | } else { 103 | // For the latest stable CRDB, we use the url provided in the CRDB release page. 104 | dbUrl, desiredVersion, err = getLatestStableVersionInfo() 105 | if err != nil { 106 | return dbUrl, "", err 107 | } 108 | } 109 | 110 | return dbUrl, desiredVersion, nil 111 | } 112 | 113 | // DownloadFromURL starts a download of the cockroach binary from the given URL. 114 | func DownloadFromURL(downloadURL string) (*http.Response, error) { 115 | log.Printf("GET %s", downloadURL) 116 | response, err := http.Get(downloadURL) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | if response.StatusCode != 200 { 122 | return nil, fmt.Errorf( 123 | "error downloading %s: %d (%s)", 124 | downloadURL, 125 | response.StatusCode, 126 | response.Status, 127 | ) 128 | } 129 | 130 | return response, nil 131 | } 132 | 133 | // DownloadBinary saves the latest version of CRDB into a local binary file, 134 | // and returns the path for this local binary. 135 | // To download a specific cockroach version, specify desiredVersion. Otherwise, 136 | // the latest stable or non-stable version will be chosen. 137 | // To download the latest STABLE version of CRDB, set `nonStable` to false. 138 | // To download the bleeding edge version of CRDB, set `nonStable` to true. 139 | func DownloadBinary(tc *TestConfig, desiredVersion string, nonStable bool) (string, error) { 140 | dbUrl, desiredVersion, err := GetDownloadURL(desiredVersion, nonStable) 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | filename, err := GetDownloadFilename(desiredVersion) 146 | if err != nil { 147 | return "", err 148 | } 149 | localFile := filepath.Join(os.TempDir(), filename) 150 | 151 | // Short circuit if the file already exists and is in the finished state. 152 | info, err := os.Stat(localFile) 153 | if err == nil && info.Mode().Perm() == finishedFileMode { 154 | return localFile, nil 155 | } 156 | 157 | response, err := DownloadFromURL(dbUrl) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | defer func() { _ = response.Body.Close() }() 163 | 164 | for { 165 | info, err := os.Stat(localFile) 166 | if os.IsNotExist(err) { 167 | // File does not exist: download it. 168 | break 169 | } 170 | if err != nil { 171 | return "", err 172 | } 173 | // File already present: check mode. 174 | if info.Mode().Perm() == finishedFileMode { 175 | return localFile, nil 176 | } 177 | 178 | localFileLock := flock.New(localFile) 179 | // If there's a process downloading the binary, local file cannot be flocked. 180 | locked, err := localFileLock.TryLock() 181 | if err != nil { 182 | return "", err 183 | } 184 | 185 | if locked { 186 | // If local file can be locked, it means the previous download was 187 | // killed in the middle. Delete local file and re-download. 188 | log.Printf("previous download failed in the middle, deleting and re-downloading") 189 | if err := os.Remove(localFile); err != nil { 190 | log.Printf("failed to remove partial download %s: %v", localFile, err) 191 | return "", err 192 | } 193 | break 194 | } 195 | 196 | log.Printf("waiting for download of %s", localFile) 197 | time.Sleep(time.Millisecond * 10) 198 | } 199 | 200 | output, err := os.OpenFile(localFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, writingFileMode) 201 | if err != nil { 202 | return "", fmt.Errorf("error creating %s: %w", localFile, err) 203 | } 204 | 205 | // Assign a flock to the local file. 206 | // If the downloading process is killed in the middle, 207 | // the lock will be automatically dropped. 208 | localFileLock := flock.New(localFile) 209 | 210 | if _, err := localFileLock.TryLock(); err != nil { 211 | return "", err 212 | } 213 | 214 | defer func() { _ = localFileLock.Unlock() }() 215 | 216 | if tc.IsTest && tc.StopDownloadInMiddle { 217 | log.Printf("download process killed") 218 | output.Close() 219 | return "", errStoppedInMiddle 220 | } 221 | 222 | var downloadMethod func(*http.Response, *os.File, string) error 223 | 224 | if nonStable { 225 | downloadMethod = downloadBinaryFromResponse 226 | } else { 227 | if runtime.GOOS == "windows" { 228 | downloadMethod = downloadBinaryFromZip 229 | } else { 230 | downloadMethod = downloadBinaryFromTar 231 | } 232 | } 233 | log.Printf("saving %s to %s, this may take some time", response.Request.URL, localFile) 234 | if err := downloadMethod(response, output, localFile); err != nil { 235 | if !errors.Is(err, errStoppedInMiddle) { 236 | if err := os.Remove(localFile); err != nil { 237 | log.Printf("failed to remove %s: %s", localFile, err) 238 | } 239 | } 240 | return "", err 241 | } 242 | 243 | if err := localFileLock.Unlock(); err != nil { 244 | return "", err 245 | } 246 | 247 | if err := output.Close(); err != nil { 248 | return "", err 249 | } 250 | 251 | return localFile, nil 252 | } 253 | 254 | // GetDownloadFilename returns the local filename of the downloaded CRDB binary file. 255 | func GetDownloadFilename(desiredVersion string) (string, error) { 256 | filename := fmt.Sprintf("cockroach-%s", desiredVersion) 257 | if runtime.GOOS == "windows" { 258 | filename += ".exe" 259 | } 260 | return filename, nil 261 | } 262 | 263 | // Release contains the information we extract from the YAML file in 264 | // `releaseDataURL`. 265 | type Release struct { 266 | Name string `yaml:"release_name"` 267 | Withdrawn bool `yaml:"withdrawn"` 268 | CloudOnly bool `yaml:"cloud_only"` 269 | } 270 | 271 | // getLatestStableVersionInfo returns the latest stable CRDB's download URL, 272 | // and the formatted corresponding version number. The download URL is based 273 | // on the runtime OS. 274 | // Note that it may return a withdrawn version, but the risk is low for local tests here. 275 | func getLatestStableVersionInfo() (string, string, error) { 276 | resp, err := http.Get(releaseDataURL) 277 | if err != nil { 278 | return "", "", fmt.Errorf("could not download release data: %w", err) 279 | } 280 | defer resp.Body.Close() 281 | 282 | var blob bytes.Buffer 283 | if _, err := io.Copy(&blob, resp.Body); err != nil { 284 | return "", "", fmt.Errorf("error reading response body: %w", err) 285 | } 286 | 287 | var data []Release 288 | if err := yaml.Unmarshal(blob.Bytes(), &data); err != nil { //nolint:yaml 289 | return "", "", fmt.Errorf("failed to YAML parse release data: %w", err) 290 | } 291 | 292 | latestStableVersion := version.MustParse("v0.0.0") 293 | 294 | for _, r := range data { 295 | // We ignore versions that cannot be parsed; this should 296 | // correspond to really old beta releases. 297 | v, err := version.Parse(r.Name) 298 | if err != nil { 299 | continue 300 | } 301 | 302 | // Skip cloud-only releases, since they cannot be downloaded from 303 | // binaries.cockroachdb.com. 304 | if r.CloudOnly { 305 | continue 306 | } 307 | 308 | // Ignore any withdrawn releases, since they are known to be broken. 309 | if r.Withdrawn { 310 | continue 311 | } 312 | 313 | // Ignore alphas, betas, and RCs. 314 | if v.PreRelease() != "" { 315 | continue 316 | } 317 | 318 | if v.Compare(latestStableVersion) > 0 { 319 | latestStableVersion = v 320 | } 321 | } 322 | 323 | downloadUrl := getDownloadUrlForVersion(latestStableVersion.String()) 324 | 325 | latestStableVerFormatted := strings.ReplaceAll(latestStableVersion.String(), ".", "-") 326 | return downloadUrl, latestStableVerFormatted, nil 327 | } 328 | 329 | func getDownloadUrlForVersion(version string) string { 330 | switch runtime.GOOS { 331 | case "linux": 332 | return fmt.Sprintf(linuxUrlpat, version, runtime.GOARCH) 333 | case "darwin": 334 | switch runtime.GOARCH { 335 | case "arm64": 336 | return fmt.Sprintf(macUrlpat, version, "11.0", runtime.GOARCH) 337 | case "amd64": 338 | return fmt.Sprintf(macUrlpat, version, "10.9", runtime.GOARCH) 339 | } 340 | case "windows": 341 | return fmt.Sprintf(winUrlpat, version) 342 | } 343 | 344 | panic(errors.New("could not get supported go os version")) 345 | } 346 | 347 | // downloadBinaryFromResponse copies the http response's body directly into a local binary. 348 | func downloadBinaryFromResponse(response *http.Response, output *os.File, filePath string) error { 349 | if _, err := io.Copy(output, response.Body); err != nil { 350 | return fmt.Errorf("problem saving %s to %s: %w", response.Request.URL, filePath, err) 351 | } 352 | 353 | // Download was successful, add the rw bits. 354 | if err := output.Chmod(finishedFileMode); err != nil { 355 | return err 356 | } 357 | 358 | return nil 359 | } 360 | 361 | // downloadBinaryFromTar writes the binary compressed in a tar from a http response 362 | // to a local file. 363 | // It is created because the download url from the release page only provides the tar.gz/zip 364 | // for a pre-compiled binary. 365 | func downloadBinaryFromTar(response *http.Response, output *os.File, filePath string) error { 366 | // Unzip the tar file from the response's body. 367 | gzf, err := gzip.NewReader(response.Body) 368 | if err != nil { 369 | return fmt.Errorf("cannot read tar from response body: %w", err) 370 | } 371 | // Read the files from the tar. 372 | tarReader := tar.NewReader(gzf) 373 | for { 374 | header, err := tarReader.Next() 375 | 376 | // No more file from tar to read. 377 | if err == io.EOF { 378 | return fmt.Errorf("cannot find the binary from tar") 379 | } 380 | 381 | if err != nil { 382 | return fmt.Errorf("cannot untar: %w", err) 383 | } 384 | 385 | // Only copy the cockroach binary. 386 | // The header.Name is of the form "zip_name/file_name". 387 | // We extract the file name. 388 | splitHeaderName := strings.Split(header.Name, "/") 389 | fileName := splitHeaderName[len(splitHeaderName)-1] 390 | if fileName == "cockroach" { 391 | // Copy the binary to desired path. 392 | if _, err := io.Copy(output, tarReader); err != nil { 393 | return fmt.Errorf( 394 | "problem saving %s to %s: %w", 395 | response.Request.URL, filePath, 396 | err, 397 | ) 398 | } 399 | if err := output.Chmod(finishedFileMode); err != nil { 400 | return err 401 | } 402 | return nil 403 | } 404 | 405 | } 406 | return nil 407 | } 408 | 409 | // downloadBinaryFromZip writes the binary compressed in a zip from a http response 410 | // to a local file. 411 | // It is created because the download url from the release page only provides the tar.gz/zip 412 | // for a pre-compiled binary. 413 | func downloadBinaryFromZip(response *http.Response, output *os.File, filePath string) error { 414 | body, err := io.ReadAll(response.Body) 415 | if err != nil { 416 | return fmt.Errorf("cannot read zip from response body: %w", err) 417 | } 418 | 419 | zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) 420 | if err != nil { 421 | log.Fatal(err) 422 | } 423 | 424 | findFile := false 425 | // Read all the files from zip archive. 426 | for _, zipFile := range zipReader.File { 427 | splitHeaderName := strings.Split(zipFile.Name, "/") 428 | fileName := splitHeaderName[len(splitHeaderName)-1] 429 | fmt.Printf("filename=%s", fileName) 430 | if fileName == "cockroach" { 431 | findFile = true 432 | if err := readZipFile(zipFile, output); err != nil { 433 | return fmt.Errorf("problem saving %s to %s: %w", 434 | response.Request.URL, 435 | filePath, 436 | err) 437 | } 438 | if err := output.Chmod(finishedFileMode); err != nil { 439 | return err 440 | } 441 | } 442 | } 443 | if !findFile { 444 | return fmt.Errorf("cannot find the binary from zip") 445 | } 446 | 447 | return nil 448 | } 449 | 450 | func readZipFile(zf *zip.File, target *os.File) error { 451 | f, err := zf.Open() 452 | if err != nil { 453 | return err 454 | } 455 | defer f.Close() 456 | if _, err = io.Copy(target, f); err != nil { 457 | return err 458 | } 459 | return nil 460 | } 461 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /testserver/testserver.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 provides helpers to run a cockroach binary within tests. 16 | // It automatically downloads the latest cockroach binary for your platform, 17 | // or attempts to run "cockroach" from your PATH. 18 | // 19 | // To use, run as follows: 20 | // 21 | // import "github.com/cockroachdb/cockroach-go/v2/testserver" 22 | // import "testing" 23 | // import "time" 24 | // 25 | // func TestRunServer(t *testing.T) { 26 | // ts, err := testserver.NewTestServer() 27 | // if err != nil { 28 | // t.Fatal(err) 29 | // } 30 | // defer ts.Stop() 31 | // 32 | // db, err := sql.Open("postgres", ts.PGURL().String()) 33 | // if err != nil { 34 | // t.Fatal(err) 35 | // } 36 | // } 37 | package testserver 38 | 39 | import ( 40 | "bufio" 41 | "bytes" 42 | "database/sql" 43 | "errors" 44 | "flag" 45 | "fmt" 46 | "log" 47 | "net/url" 48 | "os" 49 | "os/exec" 50 | "os/user" 51 | "path/filepath" 52 | "strconv" 53 | "strings" 54 | "sync" 55 | "testing" 56 | "time" 57 | 58 | "github.com/cockroachdb/cockroach-go/v2/testserver/version" 59 | // Import postgres driver. 60 | _ "github.com/lib/pq" 61 | ) 62 | 63 | var customBinaryFlag = flag.String("cockroach-binary", "", "Use specified cockroach binary") 64 | 65 | const ( 66 | stateNew = 1 + iota 67 | stateRunning 68 | stateStopped 69 | stateFailed 70 | ) 71 | 72 | const ( 73 | // First tenant ID to use is 2 since 1 belongs to the system tenant. Refer 74 | // to NewTenantServer for more information. 75 | firstTenantID = 2 76 | ) 77 | 78 | // By default, we allocate 20% of available memory to the test server. 79 | const defaultStoreMemSize = 0.2 80 | 81 | const defaultCacheSize = 0.1 82 | 83 | const defaultInitTimeout = 60 84 | const defaultPollListenURLTimeout = 60 85 | const defaultListenAddrHost = "localhost" 86 | 87 | const testserverMessagePrefix = "cockroach-go testserver" 88 | const tenantserverMessagePrefix = "cockroach-go tenantserver" 89 | 90 | // TestServer is a helper to run a real cockroach node. 91 | type TestServer interface { 92 | // Start starts the server. 93 | Start() error 94 | // Stop stops the server and cleans up any associated resources. 95 | Stop() 96 | 97 | // Stdout returns the entire contents of the first node's stdout. 98 | Stdout() string 99 | // Stdout returns the entire contents of the first node's stderr. 100 | Stderr() string 101 | // StdoutForNode returns the entire contents of the node's stdout. 102 | StdoutForNode(i int) string 103 | // StderrForNode returns the entire contents of the node's stderr. 104 | StderrForNode(i int) string 105 | // PGURL returns the postgres connection URL to this server. 106 | PGURL() *url.URL 107 | // WaitForInit retries until a SQL connection is successfully established to 108 | // this server. 109 | WaitForInit() error 110 | // BaseDir returns directory StoreOnDiskOpt writes to if used. 111 | BaseDir() string 112 | 113 | // WaitForInitFinishForNode waits until a node has completed 114 | // initialization and is available to connect to and query on. 115 | WaitForInitFinishForNode(numNode int) error 116 | // StartNode runs the "cockroach start" command for the node. 117 | StartNode(i int) error 118 | // StopNode kills the node's process. 119 | StopNode(i int) error 120 | // UpgradeNode stops the node, then starts the node on the with the 121 | // binary specified at "upgradeBinaryPath". 122 | UpgradeNode(i int) error 123 | // PGURLForNode returns the PGUrl for the node. 124 | PGURLForNode(nodeNum int) *url.URL 125 | } 126 | 127 | type pgURLChan struct { 128 | // started will be closed after the start command is executed. 129 | started chan struct{} 130 | // set will be closed once the URL is available after startup. 131 | set chan struct{} 132 | u *url.URL 133 | // The original URL is preserved here if we are using a custom password. 134 | // In that case, the one below uses client certificates, if secure (and 135 | // no password otherwise). 136 | orig url.URL 137 | } 138 | 139 | // nodeInfo contains the info to start a node and the state of the node. 140 | type nodeInfo struct { 141 | startCmd *exec.Cmd 142 | startCmdArgs []string 143 | listeningURLFile string 144 | state int 145 | stdout string 146 | stderr string 147 | stdoutBuf logWriter 148 | stderrBuf logWriter 149 | } 150 | 151 | // testServerImpl is a TestServer implementation. 152 | type testServerImpl struct { 153 | mu sync.RWMutex 154 | version *version.Version 155 | serverArgs testServerArgs 156 | serverState int 157 | baseDir string 158 | pgURL []pgURLChan 159 | initCmd *exec.Cmd 160 | initCmdArgs []string 161 | nodes []nodeInfo 162 | 163 | // curTenantID is used to allocate tenant IDs. Refer to NewTenantServer for 164 | // more information. 165 | curTenantID int 166 | proxyAddr string // empty if no sql proxy running yet 167 | proxyProcess *os.Process // empty if no sql proxy running yet 168 | } 169 | 170 | // NewDBForTest creates a new CockroachDB TestServer instance and 171 | // opens a SQL database connection to it. Returns a sql *DB instance and a 172 | // shutdown function. The caller is responsible for executing the 173 | // returned shutdown function on exit. 174 | func NewDBForTest(t *testing.T, opts ...TestServerOpt) (*sql.DB, func()) { 175 | t.Helper() 176 | return NewDBForTestWithDatabase(t, "", opts...) 177 | } 178 | 179 | // NewDBForTestWithDatabase creates a new CockroachDB TestServer 180 | // instance and opens a SQL database connection to it. If database is 181 | // specified, the returned connection will explicitly connect to 182 | // it. Returns a sql *DB instance a shutdown function. The caller is 183 | // responsible for executing the returned shutdown function on exit. 184 | func NewDBForTestWithDatabase( 185 | t *testing.T, database string, opts ...TestServerOpt, 186 | ) (*sql.DB, func()) { 187 | t.Helper() 188 | ts, err := NewTestServer(opts...) 189 | if err != nil { 190 | if errors.Is(err, errStoppedInMiddle) { 191 | // If the testserver is intentionally killed in the middle, 192 | // make sure it is stopped. 193 | return nil, func() { 194 | if ts != nil { 195 | ts.Stop() 196 | } 197 | } 198 | } 199 | t.Fatal(err) 200 | } 201 | url := ts.PGURL() 202 | if len(database) > 0 { 203 | url.Path = database 204 | } 205 | 206 | db, err := sql.Open("postgres", url.String()) 207 | if err != nil { 208 | t.Fatalf("%s: %v", testserverMessagePrefix, err) 209 | } 210 | 211 | return db, func() { 212 | _ = db.Close() 213 | ts.Stop() 214 | } 215 | } 216 | 217 | // TestServerOpt is passed to NewTestServer. 218 | type TestServerOpt func(args *testServerArgs) 219 | 220 | type TestConfig struct { 221 | IsTest bool 222 | StopDownloadInMiddle bool 223 | } 224 | 225 | type testServerArgs struct { 226 | secure bool 227 | rootPW string // if nonempty, set as pw for root 228 | storeOnDisk bool // to save database in disk 229 | storeMemSize float64 // the proportion of available memory allocated to test server in-memory store 230 | cacheSize float64 // the proportion of available memory allocated to cache 231 | httpPorts []int 232 | listenAddrPorts []int 233 | listenAddrHost string 234 | testConfig TestConfig 235 | nonStableDB bool 236 | customVersion string // custom cockroach version to use 237 | cockroachBinary string // path to cockroach executable file 238 | upgradeCockroachBinary string // path to cockroach binary for upgrade 239 | numNodes int 240 | externalIODir string 241 | initTimeoutSeconds int 242 | pollListenURLTimeoutSeconds int 243 | envVars []string // to be passed to cmd.Env 244 | localityFlags []string 245 | cockroachLogsDir string 246 | } 247 | 248 | // CockroachBinaryPathOpt is a TestServer option that can be passed to 249 | // NewTestServer to specify the path of the cockroach binary. This can be used 250 | // to avoid downloading cockroach if running tests in an environment with no 251 | // internet connection, for instance. 252 | func CockroachBinaryPathOpt(executablePath string) TestServerOpt { 253 | return func(args *testServerArgs) { 254 | args.cockroachBinary = executablePath 255 | } 256 | } 257 | 258 | func UpgradeCockroachBinaryPathOpt(executablePath string) TestServerOpt { 259 | return func(args *testServerArgs) { 260 | args.upgradeCockroachBinary = executablePath 261 | } 262 | } 263 | 264 | // SecureOpt is a TestServer option that can be passed to NewTestServer to 265 | // enable secure mode. 266 | func SecureOpt() TestServerOpt { 267 | return func(args *testServerArgs) { 268 | args.secure = true 269 | } 270 | } 271 | 272 | // StoreOnDiskOpt is a TestServer option that can be passed to NewTestServer 273 | // to enable storing database in memory. 274 | func StoreOnDiskOpt() TestServerOpt { 275 | return func(args *testServerArgs) { 276 | args.storeOnDisk = true 277 | } 278 | } 279 | 280 | // SetStoreMemSizeOpt is a TestServer option that can be passed to NewTestServer 281 | // to set the proportion of available memory that is allocated 282 | // to the test server. 283 | func SetStoreMemSizeOpt(memSize float64) TestServerOpt { 284 | return func(args *testServerArgs) { 285 | if memSize > 0 { 286 | args.storeMemSize = memSize 287 | } else { 288 | args.storeMemSize = defaultStoreMemSize 289 | } 290 | } 291 | } 292 | 293 | // CacheSizeOpt sets the proportion of available memory that is allocated 294 | // to the CockroachDB cache. 295 | func CacheSizeOpt(cacheSize float64) TestServerOpt { 296 | return func(args *testServerArgs) { 297 | if cacheSize > 0 { 298 | args.cacheSize = cacheSize 299 | } else { 300 | args.cacheSize = defaultCacheSize 301 | } 302 | } 303 | } 304 | 305 | // RootPasswordOpt is a TestServer option that, when passed to NewTestServer, 306 | // sets the given password for the root user (and returns a URL using it from 307 | // PGURL(). This avoids having to use client certs. 308 | func RootPasswordOpt(pw string) TestServerOpt { 309 | return func(args *testServerArgs) { 310 | args.rootPW = pw 311 | } 312 | } 313 | 314 | // NonStableDbOpt is a TestServer option that can be passed to NewTestServer to 315 | // download the latest beta version of CRDB, but not necessary a stable one. 316 | func NonStableDbOpt() TestServerOpt { 317 | return func(args *testServerArgs) { 318 | args.nonStableDB = true 319 | } 320 | } 321 | 322 | // CustomVersionOpt is a TestServer option that can be passed to NewTestServer to 323 | // download the a specific version of CRDB. 324 | func CustomVersionOpt(ver string) TestServerOpt { 325 | return func(args *testServerArgs) { 326 | _ = version.MustParse(ver) 327 | args.customVersion = ver 328 | } 329 | } 330 | 331 | // ExposeConsoleOpt is a TestServer option that can be passed to NewTestServer to 332 | // expose the console of the server on the given port. 333 | // Warning: This is kept around for backwards compatibility, use AddHttpPortOpt 334 | // instead. 335 | func ExposeConsoleOpt(port int) TestServerOpt { 336 | return func(args *testServerArgs) { 337 | args.httpPorts = []int{port} 338 | } 339 | } 340 | 341 | // AddHttpPortOpt is a TestServer option that can be passed to NewTestServer to 342 | // specify the http ports for the Cockroach nodes. 343 | func AddHttpPortOpt(port int) TestServerOpt { 344 | return func(args *testServerArgs) { 345 | args.httpPorts = append(args.httpPorts, port) 346 | } 347 | } 348 | 349 | // AddListenAddrPortOpt is a TestServer option that can be passed to NewTestServer to 350 | // specify the ports for the Cockroach nodes. 351 | // In the case of restarting nodes, it is up to the user of TestServer to make 352 | // sure the port used here cannot be re-used. 353 | func AddListenAddrPortOpt(port int) TestServerOpt { 354 | return func(args *testServerArgs) { 355 | args.listenAddrPorts = append(args.listenAddrPorts, port) 356 | } 357 | } 358 | 359 | // ListenAddrHostOpt is a TestServer option that can be passed to 360 | // NewTestServer to specify the host for Cockroach to listen on. By default, 361 | // this is `localhost`, and the most common override is 0.0.0.0. 362 | func ListenAddrHostOpt(host string) TestServerOpt { 363 | return func(args *testServerArgs) { 364 | args.listenAddrHost = host 365 | } 366 | } 367 | 368 | // StopDownloadInMiddleOpt is a TestServer option used only in testing. 369 | // It is used to test the flock over downloaded CRDB binary. 370 | // It should not be used in production. 371 | func StopDownloadInMiddleOpt() TestServerOpt { 372 | return func(args *testServerArgs) { 373 | tc := TestConfig{IsTest: true, StopDownloadInMiddle: true} 374 | args.testConfig = tc 375 | } 376 | } 377 | 378 | func ThreeNodeOpt() TestServerOpt { 379 | return func(args *testServerArgs) { 380 | args.numNodes = 3 381 | } 382 | } 383 | 384 | // ExternalIODirOpt is a TestServer option that can be passed to NewTestServer to 385 | // specify the external IO directory to be used for the cluster. 386 | func ExternalIODirOpt(ioDir string) TestServerOpt { 387 | return func(args *testServerArgs) { 388 | args.externalIODir = ioDir 389 | } 390 | } 391 | 392 | func InitTimeoutOpt(timeout int) TestServerOpt { 393 | return func(args *testServerArgs) { 394 | args.initTimeoutSeconds = timeout 395 | } 396 | } 397 | 398 | func PollListenURLTimeoutOpt(timeout int) TestServerOpt { 399 | return func(args *testServerArgs) { 400 | args.pollListenURLTimeoutSeconds = timeout 401 | } 402 | } 403 | 404 | // LocalityFlagsOpt is used to specify the --locality flag for each node. 405 | // 406 | // Example Usage: 407 | // 408 | // localities := LocalityFlagsOpt("region=us-west", "region=us-east", "region=us-central") 409 | // server, err := NewTestServer(ThreeNodeOpt(), localities) 410 | func LocalityFlagsOpt(locality ...string) TestServerOpt { 411 | return func(args *testServerArgs) { 412 | args.localityFlags = locality 413 | } 414 | } 415 | 416 | // EnvVarOpt is a list of environment variables to be passed to the start 417 | // command. Each entry in the slice should be in `key=value` format. 418 | func EnvVarOpt(vars []string) TestServerOpt { 419 | return func(args *testServerArgs) { 420 | args.envVars = vars 421 | } 422 | } 423 | 424 | // CockroachLogsDirOpt allows callers to control where the stdout and 425 | // stderr of cockroach processes created by the testserver are 426 | // located. Files will be in the format: $nodeID/cockroach.std{out,err}. 427 | func CockroachLogsDirOpt(dir string) TestServerOpt { 428 | return func(args *testServerArgs) { 429 | args.cockroachLogsDir = dir 430 | } 431 | } 432 | 433 | const ( 434 | logsDirName = "logs" 435 | certsDirName = "certs" 436 | ) 437 | 438 | var errStoppedInMiddle = errors.New("download stopped in middle") 439 | 440 | // NewTestServer creates a new TestServer and starts it. 441 | // It also waits until the server is ready to accept clients, 442 | // so it safe to connect to the server returned by this function right away. 443 | // The cockroach binary for your OS and ARCH is downloaded automatically. 444 | // If the download fails, we attempt just call "cockroach", hoping it is 445 | // found in your path. 446 | func NewTestServer(opts ...TestServerOpt) (TestServer, error) { 447 | baseDir, err := os.MkdirTemp("", "cockroach-testserver") 448 | if err != nil { 449 | return nil, fmt.Errorf("%s: could not create temp directory: %w", testserverMessagePrefix, err) 450 | } 451 | 452 | serverArgs := &testServerArgs{numNodes: 1} 453 | serverArgs.storeMemSize = defaultStoreMemSize 454 | serverArgs.cacheSize = defaultCacheSize 455 | serverArgs.initTimeoutSeconds = defaultInitTimeout 456 | serverArgs.pollListenURLTimeoutSeconds = defaultPollListenURLTimeout 457 | serverArgs.listenAddrHost = defaultListenAddrHost 458 | serverArgs.cockroachLogsDir = baseDir 459 | for _, applyOptToArgs := range opts { 460 | applyOptToArgs(serverArgs) 461 | } 462 | log.Printf("cockroach logs directory: %s", serverArgs.cockroachLogsDir) 463 | 464 | if serverArgs.cockroachBinary != "" { 465 | // CockroachBinaryPathOpt() overrides the flag or env variable. 466 | } else if len(*customBinaryFlag) > 0 { 467 | serverArgs.cockroachBinary = *customBinaryFlag 468 | } else if customBinaryEnv := os.Getenv("COCKROACH_BINARY"); customBinaryEnv != "" { 469 | serverArgs.cockroachBinary = customBinaryEnv 470 | } 471 | 472 | if len(serverArgs.listenAddrPorts) == 0 { 473 | serverArgs.listenAddrPorts = make([]int, serverArgs.numNodes) 474 | } 475 | if serverArgs.numNodes != 1 && len(serverArgs.listenAddrPorts) != serverArgs.numNodes { 476 | panic(fmt.Sprintf("need to specify a port for each node using AddListenAddrPortOpt, got %d nodes, need %d ports", 477 | serverArgs.numNodes, len(serverArgs.listenAddrPorts))) 478 | } 479 | 480 | if 0 < len(serverArgs.localityFlags) && len(serverArgs.localityFlags) != serverArgs.numNodes { 481 | panic(fmt.Sprintf("got %d locality flags when %d are needed (one for each node)", len(serverArgs.localityFlags), serverArgs.numNodes)) 482 | } 483 | 484 | if serverArgs.cockroachBinary != "" { 485 | log.Printf("Using custom cockroach binary: %s", serverArgs.cockroachBinary) 486 | cockroachBinary, err := filepath.Abs(serverArgs.cockroachBinary) 487 | if err == nil { 488 | // Update path to absolute. 489 | serverArgs.cockroachBinary = cockroachBinary 490 | } 491 | } else { 492 | serverArgs.cockroachBinary, err = DownloadBinary(&serverArgs.testConfig, serverArgs.customVersion, serverArgs.nonStableDB) 493 | if err != nil { 494 | if errors.Is(err, errStoppedInMiddle) { 495 | // If the testserver is intentionally killed in the middle of downloading, 496 | // return error. 497 | return nil, err 498 | } 499 | log.Printf("%s: Failed to fetch latest binary: %v attempting to use cockroach binary from your PATH", testserverMessagePrefix, err) 500 | serverArgs.cockroachBinary = "cockroach" 501 | } else { 502 | log.Printf("Using automatically-downloaded binary: %s", serverArgs.cockroachBinary) 503 | } 504 | } 505 | 506 | mkDir := func(name string) (string, error) { 507 | path := filepath.Join(baseDir, name) 508 | if err := os.MkdirAll(path, 0755); err != nil { 509 | return "", fmt.Errorf("%s: could not create %s directory: %s: %w", 510 | testserverMessagePrefix, name, path, err) 511 | } 512 | return path, nil 513 | } 514 | certsDir, err := mkDir(certsDirName) 515 | if err != nil { 516 | return nil, fmt.Errorf("%s: %w", testserverMessagePrefix, err) 517 | } 518 | 519 | secureOpt := "--insecure" 520 | if serverArgs.secure { 521 | // Create certificates. 522 | certArgs := []string{ 523 | "--certs-dir=" + certsDir, 524 | "--ca-key=" + filepath.Join(certsDir, "ca.key"), 525 | } 526 | for _, args := range [][]string{ 527 | // Create the CA cert and key pair. 528 | {"cert", "create-ca"}, 529 | // Create cert and key pair for the cockroach node. 530 | {"cert", "create-node", "localhost"}, 531 | // Create cert and key pair for the root user (SQL client). 532 | {"cert", "create-client", "root", "--also-generate-pkcs8-key"}, 533 | } { 534 | createCertCmd := exec.Command(serverArgs.cockroachBinary, append(args, certArgs...)...) 535 | log.Printf("%s executing: %s", testserverMessagePrefix, createCertCmd) 536 | if err := createCertCmd.Run(); err != nil { 537 | return nil, fmt.Errorf("%s command %s failed: %w", testserverMessagePrefix, createCertCmd, err) 538 | } 539 | } 540 | secureOpt = "--certs-dir=" + certsDir 541 | } 542 | 543 | // v19.1 and earlier do not have the `start-single-node` subcommand, 544 | // so use `start` for those versions. 545 | // TODO(rafi): Remove the version check and `start` once we stop testing 19.1. 546 | versionCmd := exec.Command(serverArgs.cockroachBinary, "version") 547 | versionOutput, err := versionCmd.CombinedOutput() 548 | if err != nil { 549 | return nil, fmt.Errorf("%s command %s failed: %w", testserverMessagePrefix, versionCmd, err) 550 | } 551 | reader := bufio.NewReader(bytes.NewReader(versionOutput)) 552 | versionLine, err := reader.ReadString('\n') 553 | if err != nil { 554 | return nil, fmt.Errorf("%s failed to read version: %w", testserverMessagePrefix, err) 555 | } 556 | versionLineTokens := strings.Fields(versionLine) 557 | v, err := version.Parse(versionLineTokens[2]) 558 | if err != nil { 559 | return nil, fmt.Errorf("%s failed to parse version: %w", testserverMessagePrefix, err) 560 | } 561 | 562 | startCmd := "start-single-node" 563 | if !v.AtLeast(version.MustParse("v19.2.0-alpha")) || serverArgs.numNodes > 1 { 564 | startCmd = "start" 565 | } 566 | 567 | nodes := make([]nodeInfo, serverArgs.numNodes) 568 | if len(serverArgs.httpPorts) == 0 { 569 | serverArgs.httpPorts = make([]int, serverArgs.numNodes) 570 | } 571 | 572 | if serverArgs.externalIODir == "" { 573 | serverArgs.externalIODir = "disabled" 574 | } 575 | 576 | for i := 0; i < serverArgs.numNodes; i++ { 577 | storeArg := fmt.Sprintf("--store=type=mem,size=%.2f", serverArgs.storeMemSize) 578 | logsBaseDir := filepath.Join(serverArgs.cockroachLogsDir, strconv.Itoa(i)) 579 | nodeBaseDir, err := mkDir(strconv.Itoa(i)) 580 | if err != nil { 581 | return nil, err 582 | } 583 | if serverArgs.storeOnDisk { 584 | storeArg = fmt.Sprintf("--store=path=%s", nodeBaseDir) 585 | } 586 | // TODO(janexing): Make sure the log is written to logDir instead of shown in console. 587 | // Should be done once issue #109 is solved: 588 | // https://github.com/cockroachdb/cockroach-go/issues/109 589 | nodes[i].stdout = filepath.Join(logsBaseDir, "cockroach.stdout") 590 | nodes[i].stderr = filepath.Join(logsBaseDir, "cockroach.stderr") 591 | nodes[i].listeningURLFile = filepath.Join(nodeBaseDir, "listen-url") 592 | nodes[i].state = stateNew 593 | if serverArgs.numNodes > 1 { 594 | nodes[i].startCmdArgs = []string{ 595 | serverArgs.cockroachBinary, 596 | startCmd, 597 | "--logtostderr", 598 | secureOpt, 599 | storeArg, 600 | fmt.Sprintf( 601 | "--listen-addr=%s:%d", 602 | serverArgs.listenAddrHost, 603 | serverArgs.listenAddrPorts[i], 604 | ), 605 | fmt.Sprintf( 606 | "--http-addr=%s:%d", 607 | serverArgs.listenAddrHost, 608 | serverArgs.httpPorts[i], 609 | ), 610 | "--listening-url-file=" + nodes[i].listeningURLFile, 611 | "--external-io-dir=" + serverArgs.externalIODir, 612 | } 613 | } else { 614 | nodes[0].startCmdArgs = []string{ 615 | serverArgs.cockroachBinary, 616 | startCmd, 617 | "--logtostderr", 618 | secureOpt, 619 | fmt.Sprintf("--host=%s", serverArgs.listenAddrHost), 620 | "--port=" + strconv.Itoa(serverArgs.listenAddrPorts[0]), 621 | "--http-port=" + strconv.Itoa(serverArgs.httpPorts[0]), 622 | storeArg, 623 | "--cache=" + strconv.FormatFloat(serverArgs.cacheSize, 'f', 4, 64), 624 | "--listening-url-file=" + nodes[i].listeningURLFile, 625 | "--external-io-dir=" + serverArgs.externalIODir, 626 | } 627 | } 628 | if 0 < len(serverArgs.localityFlags) { 629 | nodes[i].startCmdArgs = append(nodes[i].startCmdArgs, fmt.Sprintf("--locality=%s", serverArgs.localityFlags[i])) 630 | } 631 | } 632 | 633 | // We only need initArgs if we're creating a testserver 634 | // with multiple nodes. 635 | initArgs := []string{ 636 | serverArgs.cockroachBinary, 637 | "init", 638 | secureOpt, 639 | } 640 | 641 | states := make([]int, serverArgs.numNodes) 642 | for i := 0; i < serverArgs.numNodes; i++ { 643 | states[i] = stateNew 644 | } 645 | 646 | ts := &testServerImpl{ 647 | serverArgs: *serverArgs, 648 | version: v, 649 | serverState: stateNew, 650 | baseDir: baseDir, 651 | initCmdArgs: initArgs, 652 | curTenantID: firstTenantID, 653 | nodes: nodes, 654 | } 655 | ts.pgURL = make([]pgURLChan, serverArgs.numNodes) 656 | for i := range ts.pgURL { 657 | ts.pgURL[i].started = make(chan struct{}) 658 | ts.pgURL[i].set = make(chan struct{}) 659 | } 660 | 661 | if err := ts.Start(); err != nil { 662 | return nil, fmt.Errorf("%s Start failed: %w", testserverMessagePrefix, err) 663 | } 664 | 665 | if ts.PGURL() == nil { 666 | return nil, fmt.Errorf("%s: url not found", testserverMessagePrefix) 667 | } 668 | 669 | if err := ts.WaitForInit(); err != nil { 670 | return nil, fmt.Errorf("%s WaitForInit failed: %w", testserverMessagePrefix, err) 671 | } 672 | 673 | return ts, nil 674 | } 675 | 676 | // Stdout returns the entire contents of the first node stdout. 677 | func (ts *testServerImpl) Stdout() string { 678 | return ts.StdoutForNode(0) 679 | } 680 | 681 | // Stderr returns the entire contents of the first node stderr. 682 | func (ts *testServerImpl) Stderr() string { 683 | return ts.StderrForNode(0) 684 | } 685 | 686 | // StdoutForNode returns the entire contents of the node's stdout. 687 | func (ts *testServerImpl) StdoutForNode(i int) string { 688 | return ts.nodes[i].stdoutBuf.String() 689 | } 690 | 691 | // StderrForNode returns the entire contents of the node's stderr. 692 | func (ts *testServerImpl) StderrForNode(i int) string { 693 | return ts.nodes[i].stderrBuf.String() 694 | } 695 | 696 | // BaseDir returns directory StoreOnDiskOpt writes to if used. 697 | func (ts *testServerImpl) BaseDir() string { 698 | return ts.baseDir 699 | } 700 | 701 | // PGURL returns the postgres connection URL to reach the started 702 | // cockroach node. 703 | // 704 | // It blocks until the network URL is determined and does not timeout, 705 | // relying instead on test timeouts. 706 | func (ts *testServerImpl) PGURL() *url.URL { 707 | return ts.PGURLForNode(0) 708 | } 709 | 710 | func (ts *testServerImpl) setPGURL(u *url.URL) { 711 | ts.setPGURLForNode(0, u) 712 | } 713 | 714 | func (ts *testServerImpl) PGURLForNode(nodeNum int) *url.URL { 715 | <-ts.pgURL[nodeNum].set 716 | return ts.pgURL[nodeNum].u 717 | } 718 | 719 | func (ts *testServerImpl) setPGURLForNode(nodeNum int, u *url.URL) { 720 | ts.pgURL[nodeNum].u = u 721 | close(ts.pgURL[nodeNum].set) 722 | } 723 | 724 | func (ts *testServerImpl) WaitForInitFinishForNode(nodeIdx int) error { 725 | pgURL := ts.PGURLForNode(nodeIdx).String() 726 | for i := 0; i < ts.serverArgs.initTimeoutSeconds*10; i++ { 727 | err := func() error { 728 | db, err := sql.Open("postgres", pgURL) 729 | if err != nil { 730 | return err 731 | } 732 | defer func() { _ = db.Close() }() 733 | var s string 734 | if err := db.QueryRow("SELECT 'started'").Scan(&s); err != nil { 735 | return err 736 | } 737 | if s != "started" { 738 | return fmt.Errorf("healthcheck query had incorrect result") 739 | } 740 | return nil 741 | }() 742 | if err == nil { 743 | return nil 744 | } 745 | log.Printf("%s: WaitForInitFinishForNode %d (%s): Trying again after error: %v", testserverMessagePrefix, nodeIdx, pgURL, err) 746 | time.Sleep(time.Millisecond * 100) 747 | } 748 | log.Printf( 749 | "init did not finish for node %d\n\nstdout:\n%s\n\nstderr:\n:%s", 750 | nodeIdx, 751 | ts.StdoutForNode(nodeIdx), 752 | ts.StderrForNode(nodeIdx), 753 | ) 754 | return fmt.Errorf("init did not finish for node %d", nodeIdx) 755 | } 756 | 757 | // WaitForInit retries until a connection is successfully established. 758 | func (ts *testServerImpl) WaitForInit() error { 759 | return ts.WaitForInitFinishForNode(0) 760 | } 761 | 762 | func (ts *testServerImpl) pollListeningURLFile(nodeNum int) error { 763 | log.Printf("node %d: waiting for listening URL file %q\n", nodeNum, ts.nodes[nodeNum].listeningURLFile) 764 | var data []byte 765 | var err error 766 | maxWait := time.Duration(ts.serverArgs.pollListenURLTimeoutSeconds) * time.Second 767 | for startTime := time.Now(); ; { 768 | ts.mu.RLock() 769 | state := ts.nodes[nodeNum].state 770 | ts.mu.RUnlock() 771 | if state != stateRunning { 772 | return fmt.Errorf("node %d stopped or crashed before listening URL file was available", nodeNum) 773 | } 774 | data, err = os.ReadFile(ts.nodes[nodeNum].listeningURLFile) 775 | // There are two cases where we want to retry: 776 | // - the file does not exist yet. 777 | // - the file exists but it is empty (we probably raced with the file being written). 778 | if (os.IsNotExist(err) || (err == nil && len(data) == 0)) && time.Since(startTime) < maxWait { 779 | time.Sleep(100 * time.Millisecond) 780 | continue 781 | } 782 | break 783 | } 784 | 785 | if err != nil { 786 | if !os.IsNotExist(err) { 787 | return fmt.Errorf("node %d: unexpected error while reading listening URL file %q: %w", nodeNum, ts.nodes[nodeNum].listeningURLFile, err) 788 | } 789 | return fmt.Errorf("node %d: file %q did not show up afer %d seconds", nodeNum, ts.nodes[nodeNum].listeningURLFile, ts.serverArgs.pollListenURLTimeoutSeconds) 790 | } 791 | if len(data) == 0 { 792 | return fmt.Errorf("node %d: listening URL file %q is empty", nodeNum, ts.nodes[nodeNum].listeningURLFile) 793 | } 794 | 795 | u, err := url.Parse(string(bytes.TrimSpace(data))) 796 | if err != nil { 797 | return fmt.Errorf("failed to parse SQL URL: %w", err) 798 | } 799 | log.Printf("node %d: got URL %s\n", nodeNum, u) 800 | 801 | ts.pgURL[nodeNum].orig = *u 802 | if pw := ts.serverArgs.rootPW; pw != "" { 803 | db, err := sql.Open("postgres", u.String()) 804 | if err != nil { 805 | return err 806 | } 807 | defer db.Close() 808 | if _, err := db.Exec(`ALTER USER root WITH PASSWORD $1`, pw); err != nil { 809 | return err 810 | } 811 | 812 | v := u.Query() 813 | v.Del("sslkey") 814 | v.Del("sslcert") 815 | u.RawQuery = v.Encode() 816 | u.User = url.UserPassword("root", pw) 817 | } 818 | 819 | ts.setPGURLForNode(nodeNum, u) 820 | 821 | return nil 822 | } 823 | 824 | // Start runs the process, returning an error on any problems, 825 | // including being unable to start, but not unexpected failure. 826 | // It should only be called once in the lifetime of a TestServer object. 827 | // If the server is already running, this function is a no-op. 828 | // If the server stopped or failed, please don't use ts.Start() 829 | // to restart a testserver, but use NewTestServer(). 830 | func (ts *testServerImpl) Start() error { 831 | ts.mu.Lock() 832 | if ts.serverState != stateNew { 833 | ts.mu.Unlock() 834 | switch ts.serverState { 835 | case stateRunning: 836 | return nil // No-op if server is already running. 837 | case stateStopped, stateFailed: 838 | // Start() can only be called once. 839 | return errors.New( 840 | "`Start()` cannot be used to restart a stopped or failed server. " + 841 | "Please use NewTestServer()") 842 | } 843 | } 844 | ts.serverState = stateRunning 845 | ts.mu.Unlock() 846 | 847 | for i := 0; i < ts.serverArgs.numNodes; i++ { 848 | if err := ts.StartNode(i); err != nil { 849 | return err 850 | } 851 | } 852 | 853 | if ts.serverArgs.numNodes > 1 { 854 | err := ts.CockroachInit() 855 | if err != nil { 856 | return err 857 | } 858 | } 859 | 860 | return nil 861 | } 862 | 863 | // Stop kills the process if it is still running and cleans its directory. 864 | // It should only be called once in the lifetime of a TestServer object. 865 | // Logs fatal if the process has already failed. 866 | func (ts *testServerImpl) Stop() { 867 | ts.mu.RLock() 868 | defer ts.mu.RUnlock() 869 | 870 | if ts.serverState == stateNew { 871 | log.Fatalf("%s: Stop() called, but Start() was never called", testserverMessagePrefix) 872 | } 873 | if ts.serverState == stateFailed { 874 | log.Fatalf("%s: Stop() called, but process exited unexpectedly. Stdout:\n%s\nStderr:\n%s\n", 875 | testserverMessagePrefix, 876 | ts.Stdout(), 877 | ts.Stderr()) 878 | } 879 | 880 | if ts.serverState != stateStopped { 881 | if p := ts.proxyProcess; p != nil { 882 | _ = p.Kill() 883 | } 884 | } 885 | 886 | ts.serverState = stateStopped 887 | for i, node := range ts.nodes { 888 | cmd := node.startCmd 889 | if cmd.Process != nil { 890 | _ = cmd.Process.Kill() 891 | } 892 | 893 | if node.state != stateFailed { 894 | node.state = stateStopped 895 | } 896 | 897 | if node.state != stateStopped { 898 | ts.serverState = stateFailed 899 | } 900 | 901 | // RUnlock such that StopNode can Lock and Unlock. 902 | ts.mu.RUnlock() 903 | err := ts.StopNode(i) 904 | if err != nil { 905 | log.Printf("error stopping node %d: %s", i, err) 906 | } 907 | ts.mu.RLock() 908 | 909 | nodeDir := filepath.Join(ts.baseDir, strconv.Itoa(i)) 910 | if err := os.RemoveAll(nodeDir); err != nil { 911 | log.Printf("error deleting tmp directory %s for node: %s", nodeDir, err) 912 | } 913 | if closeErr := ts.nodes[i].stdoutBuf.Close(); closeErr != nil { 914 | log.Printf("%s: failed to close stdout: %v", testserverMessagePrefix, closeErr) 915 | } 916 | if closeErr := ts.nodes[i].stderrBuf.Close(); closeErr != nil { 917 | log.Printf("%s: failed to close stderr: %v", testserverMessagePrefix, closeErr) 918 | } 919 | } 920 | 921 | // Only cleanup on intentional stops. 922 | _ = os.RemoveAll(ts.baseDir) 923 | } 924 | 925 | func (ts *testServerImpl) CockroachInit() error { 926 | // The port must be computed here, since it may not be known until after 927 | // a node is started (if the listen port is 0). 928 | args := append(ts.initCmdArgs, fmt.Sprintf("--host=localhost:%s", ts.PGURL().Port())) 929 | ts.initCmd = exec.Command(args[0], args[1:]...) 930 | ts.initCmd.Env = []string{ 931 | "COCKROACH_MAX_OFFSET=1ns", 932 | "COCKROACH_TRUST_CLIENT_PROVIDED_SQL_REMOTE_ADDR=true", 933 | } 934 | 935 | // Set the working directory of the cockroach process to our temp folder. 936 | // This stops cockroach from polluting the project directory with _dump 937 | // folders. 938 | ts.initCmd.Dir = ts.baseDir 939 | 940 | err := ts.initCmd.Start() 941 | if ts.initCmd.Process != nil { 942 | log.Printf("process %d started: %s", ts.initCmd.Process.Pid, strings.Join(args, " ")) 943 | } 944 | if err != nil { 945 | return err 946 | } 947 | return nil 948 | } 949 | 950 | type logWriter interface { 951 | Write(p []byte) (n int, err error) 952 | String() string 953 | Len() int64 954 | Close() error 955 | } 956 | 957 | type fileLogWriter struct { 958 | filename string 959 | file *os.File 960 | } 961 | 962 | func newFileLogWriter(file string) (*fileLogWriter, error) { 963 | if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { 964 | return nil, err 965 | } 966 | // If the file doesn't exist, create it, else append to the file. 967 | f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) 968 | if err != nil { 969 | return nil, err 970 | } 971 | 972 | return &fileLogWriter{ 973 | filename: file, 974 | file: f, 975 | }, nil 976 | } 977 | 978 | func (w fileLogWriter) Close() error { 979 | return w.file.Close() 980 | } 981 | 982 | func (w fileLogWriter) Write(p []byte) (n int, err error) { 983 | return w.file.Write(p) 984 | } 985 | 986 | func (w fileLogWriter) String() string { 987 | b, err := os.ReadFile(w.filename) 988 | if err == nil { 989 | return string(b) 990 | } 991 | return "" 992 | } 993 | 994 | func (w fileLogWriter) Len() int64 { 995 | s, err := os.Stat(w.filename) 996 | if err == nil { 997 | return s.Size() 998 | } 999 | return 0 1000 | } 1001 | 1002 | func defaultEnv() map[string]string { 1003 | vars := map[string]string{} 1004 | u, err := user.Current() 1005 | if err == nil { 1006 | if _, ok := vars["USER"]; !ok { 1007 | vars["USER"] = u.Username 1008 | } 1009 | if _, ok := vars["UID"]; !ok { 1010 | vars["UID"] = u.Uid 1011 | } 1012 | if _, ok := vars["GID"]; !ok { 1013 | vars["GID"] = u.Gid 1014 | } 1015 | if _, ok := vars["HOME"]; !ok { 1016 | vars["HOME"] = u.HomeDir 1017 | } 1018 | } 1019 | if _, ok := vars["PATH"]; !ok { 1020 | vars["PATH"] = os.Getenv("PATH") 1021 | } 1022 | return vars 1023 | } 1024 | -------------------------------------------------------------------------------- /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 | t.Run(tc.name, func(t *testing.T) { 239 | db, stop := tc.instantiation(t) 240 | defer stop() 241 | var out int 242 | row := db.QueryRow("SELECT 1") 243 | require.NoError(t, row.Scan(&out)) 244 | require.Equal(t, out, 1) 245 | _, err := db.Exec("SELECT 1") 246 | require.NoError(t, err) 247 | }) 248 | } 249 | } 250 | 251 | func TestCockroachBinaryPathOpt(t *testing.T) { 252 | crdbBinary := "doesnotexist" 253 | _, err := testserver.NewTestServer(testserver.CockroachBinaryPathOpt(crdbBinary)) 254 | if err == nil { 255 | t.Fatal("expected err, got nil") 256 | } 257 | // Confirm that the command is updated to reference the absolute path 258 | // of the custom cockroachdb binary. 259 | cmdPath, fPathErr := filepath.Abs(crdbBinary) 260 | if fPathErr != nil { 261 | cmdPath = crdbBinary 262 | } 263 | wantSubstring := fmt.Sprintf("command %s version failed", cmdPath) 264 | if msg := err.Error(); !strings.Contains(msg, wantSubstring) { 265 | t.Fatalf("error message %q does not contain %q", msg, wantSubstring) 266 | } 267 | } 268 | 269 | func TestCockroachExternalIODirOpt(t *testing.T) { 270 | externalDir, err := os.MkdirTemp("/tmp", "cockroach-testserver") 271 | require.NoError(t, err) 272 | defer func() { 273 | err := os.RemoveAll(externalDir) 274 | require.NoError(t, err) 275 | }() 276 | 277 | db, cleanup := testserver.NewDBForTest(t, testserver.ExternalIODirOpt(externalDir)) 278 | defer cleanup() 279 | 280 | // test that we can use external dir 281 | _, err = db.Exec("BACKUP INTO 'nodelocal://self/backup'") 282 | require.NoError(t, err) 283 | 284 | // test that external dir has files 285 | f, err := os.Open(externalDir) 286 | require.NoError(t, err) 287 | defer f.Close() 288 | _, err = f.Readdirnames(1) 289 | require.NoError(t, err) 290 | } 291 | 292 | func TestPGURLWhitespace(t *testing.T) { 293 | ts, err := testserver.NewTestServer() 294 | if err != nil { 295 | t.Fatal(err) 296 | } 297 | url := ts.PGURL().String() 298 | if trimmed := strings.TrimSpace(url); url != trimmed { 299 | t.Errorf("unexpected whitespace in server URL: %q", url) 300 | } 301 | } 302 | 303 | func TestSingleNodePort(t *testing.T) { 304 | port, err := getFreePort() 305 | require.NoError(t, err) 306 | 307 | ts, err := testserver.NewTestServer(testserver.AddListenAddrPortOpt(port)) 308 | if err != nil { 309 | t.Fatal(err) 310 | } 311 | defer ts.Stop() 312 | 313 | // check that port overriding worked 314 | url := ts.PGURL() 315 | require.Equal(t, strconv.Itoa(port), url.Port()) 316 | 317 | db, err := sql.Open("postgres", url.String()) 318 | require.NoError(t, err) 319 | 320 | // check that connection was successful 321 | var out int 322 | row := db.QueryRow("SELECT 1") 323 | require.NoError(t, row.Scan(&out)) 324 | require.Equal(t, 1, out) 325 | } 326 | 327 | // tenantInterface is defined in order to use tenant-related methods on the 328 | // TestServer. 329 | type tenantInterface interface { 330 | NewTenantServer(proxy bool) (testserver.TestServer, error) 331 | } 332 | 333 | // newTenantDBForTest is a testing helper function that starts a TestServer 334 | // process and a SQL tenant process pointed at this TestServer. A sql connection 335 | // to the tenant and a cleanup function are returned. 336 | func newTenantDBForTest( 337 | t *testing.T, 338 | secure bool, 339 | proxy bool, 340 | pw string, 341 | diskStore bool, 342 | storeMemSize float64, 343 | nonStableDB bool, 344 | ) (*sql.DB, func()) { 345 | t.Helper() 346 | var opts []testserver.TestServerOpt 347 | if secure { 348 | opts = append(opts, testserver.SecureOpt()) 349 | } 350 | if diskStore { 351 | opts = append(opts, testserver.StoreOnDiskOpt()) 352 | } 353 | if pw != "" { 354 | opts = append(opts, testserver.RootPasswordOpt(pw)) 355 | } 356 | if storeMemSize >= 0 { 357 | opts = append(opts, testserver.SetStoreMemSizeOpt(storeMemSize)) 358 | } else { 359 | t.Fatal("Percentage memory size for data storage cannot be nagative") 360 | } 361 | if nonStableDB { 362 | opts = append(opts, testserver.NonStableDbOpt()) 363 | } 364 | ts, err := testserver.NewTestServer(opts...) 365 | if err != nil { 366 | t.Fatal(err) 367 | } 368 | tenant, err := ts.(tenantInterface).NewTenantServer(proxy) 369 | if err != nil { 370 | t.Fatal(err) 371 | } 372 | url := tenant.PGURL() 373 | if url == nil { 374 | t.Fatal("postgres url not found") 375 | } 376 | db, err := sql.Open("postgres", url.String()) 377 | if err != nil { 378 | t.Fatal(err) 379 | } 380 | return db, func() { 381 | _ = db.Close() 382 | tenant.Stop() 383 | ts.Stop() 384 | } 385 | } 386 | 387 | func TestTenant(t *testing.T) { 388 | db, stop := newTenantDBForTest( 389 | t, 390 | false, /* secure */ 391 | false, /* proxy */ 392 | noPW, /* pw */ 393 | false, /* diskStore */ 394 | defStoreMemSize, /* storeMemSize */ 395 | false, /* nonStableDB */ 396 | ) 397 | 398 | defer stop() 399 | if _, err := db.Exec("SELECT 1"); err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | if _, err := db.Exec("SELECT crdb_internal.create_tenant(123)"); err == nil { 404 | t.Fatal("expected an error when creating a tenant since secondary tenants should not be able to do so") 405 | } 406 | } 407 | 408 | func TestFlockOnDownloadedCRDB(t *testing.T) { 409 | for _, tc := range []struct { 410 | name string 411 | instantiation func(*testing.T) (*sql.DB, func()) 412 | }{ 413 | { 414 | // It will print "waiting for download of ..." in log for about 30 seconds. 415 | name: "DownloadPassed", 416 | instantiation: func(t *testing.T) (*sql.DB, func()) { 417 | return testFlockWithDownloadPassing(t) 418 | }, 419 | }, { 420 | name: "DownloadKilled", 421 | instantiation: func(t *testing.T) (*sql.DB, func()) { 422 | return testFlockWithDownloadKilled(t) 423 | }, 424 | }, 425 | } { 426 | t.Run(tc.name, func(t *testing.T) { 427 | db, stop := tc.instantiation(t) 428 | defer stop() 429 | if _, err := db.Exec("SELECT 1"); err != nil { 430 | t.Fatal(err) 431 | } 432 | }) 433 | } 434 | } 435 | 436 | func getFreePort() (int, error) { 437 | addr, err := net.ResolveTCPAddr("tcp", ":0") 438 | if err != nil { 439 | return 0, err 440 | } 441 | 442 | l, err := net.ListenTCP("tcp", addr) 443 | if err != nil { 444 | return 0, err 445 | } 446 | port := l.Addr().(*net.TCPAddr).Port 447 | if err != nil { 448 | return 0, err 449 | } 450 | 451 | err = l.Close() 452 | if err != nil { 453 | return 0, err 454 | } 455 | 456 | return port, err 457 | } 458 | 459 | func TestRestartNodeParallel(t *testing.T) { 460 | require.NoError(t, os.Mkdir("./temp_binaries", 0755)) 461 | defer func() { 462 | require.NoError(t, os.RemoveAll("./temp_binaries")) 463 | }() 464 | var fileName string 465 | switch runtime.GOOS { 466 | case "darwin": 467 | fileName = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", "v22.1.6") 468 | require.NoError(t, downloadBinaryTest( 469 | fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 470 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 471 | case "linux": 472 | fileName = fmt.Sprintf("cockroach-%s.linux-amd64", "v22.1.6") 473 | require.NoError(t, downloadBinaryTest( 474 | fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 475 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 476 | default: 477 | t.Fatalf("unsupported os for test: %s", runtime.GOOS) 478 | } 479 | 480 | tarCmd := exec.Command("tar", "-zxvf", fmt.Sprintf("./temp_binaries/%s.tgz", fileName), "-C", "./temp_binaries") 481 | require.NoError(t, tarCmd.Start()) 482 | require.NoError(t, tarCmd.Wait()) 483 | 484 | mu := sync.Mutex{} 485 | usedPorts := make(map[int]struct{}) 486 | const ParallelExecs = 5 487 | var wg sync.WaitGroup 488 | wg.Add(ParallelExecs) 489 | for i := 0; i < ParallelExecs; i++ { 490 | go func() { 491 | ports := make([]int, 3) 492 | for j := 0; j < 3; j++ { 493 | for { 494 | port, err := getFreePort() 495 | require.NoError(t, err) 496 | mu.Lock() 497 | _, found := usedPorts[port] 498 | if !found { 499 | usedPorts[port] = struct{}{} 500 | } 501 | ports[j] = port 502 | mu.Unlock() 503 | if !found { 504 | break 505 | } 506 | } 507 | } 508 | testRestartNode(t, ports, "temp_binaries/"+fileName+"/cockroach") 509 | wg.Done() 510 | }() 511 | } 512 | wg.Wait() 513 | } 514 | 515 | func testRestartNode(t *testing.T, ports []int, binaryPath string) { 516 | const pollListenURLTimeout = 150 517 | ts, err := testserver.NewTestServer( 518 | testserver.ThreeNodeOpt(), 519 | testserver.StoreOnDiskOpt(), 520 | testserver.AddListenAddrPortOpt(ports[0]), 521 | testserver.AddListenAddrPortOpt(ports[1]), 522 | testserver.AddListenAddrPortOpt(ports[2]), 523 | testserver.CockroachBinaryPathOpt(binaryPath), 524 | testserver.PollListenURLTimeoutOpt(pollListenURLTimeout)) 525 | require.NoError(t, err) 526 | defer ts.Stop() 527 | for i := 0; i < 3; i++ { 528 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 529 | } 530 | 531 | log.Printf("Stopping Node 0") 532 | require.NoError(t, ts.StopNode(0)) 533 | for i := 1; i < 3; i++ { 534 | url := ts.PGURLForNode(i) 535 | 536 | db, err := sql.Open("postgres", url.String()) 537 | require.NoError(t, err) 538 | var out int 539 | row := db.QueryRow("SELECT 1") 540 | err = row.Scan(&out) 541 | require.NoError(t, err) 542 | require.NoError(t, db.Close()) 543 | } 544 | 545 | require.NoError(t, ts.StartNode(0)) 546 | require.NoError(t, ts.WaitForInitFinishForNode(0)) 547 | 548 | for i := 0; i < 3; i++ { 549 | url := ts.PGURLForNode(i) 550 | db, err := sql.Open("postgres", url.String()) 551 | require.NoError(t, err) 552 | 553 | var out int 554 | row := db.QueryRow("SELECT 1") 555 | err = row.Scan(&out) 556 | require.NoError(t, err) 557 | require.NoError(t, db.Close()) 558 | } 559 | 560 | url := ts.PGURLForNode(0) 561 | db, err := sql.Open("postgres", url.String()) 562 | require.NoError(t, err) 563 | var out int 564 | row := db.QueryRow("SELECT 1") 565 | err = row.Scan(&out) 566 | require.NoError(t, err) 567 | require.NoError(t, db.Close()) 568 | } 569 | 570 | func downloadBinaryTest(filepath string, url string) error { 571 | // Get the data 572 | resp, err := http.Get(url) 573 | if err != nil { 574 | return err 575 | } 576 | defer resp.Body.Close() 577 | 578 | // Create the file 579 | out, err := os.Create(filepath) 580 | if err != nil { 581 | return err 582 | } 583 | defer out.Close() 584 | 585 | // Write the body to file 586 | _, err = io.Copy(out, resp.Body) 587 | return err 588 | } 589 | 590 | func TestUpgradeNode(t *testing.T) { 591 | oldVersion := "v21.2.12" 592 | newVersion := "v22.1.6" 593 | 594 | var oldVersionBinary, newVersionBinary string 595 | switch runtime.GOOS { 596 | case "darwin": 597 | oldVersionBinary = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", oldVersion) 598 | newVersionBinary = fmt.Sprintf("cockroach-%s.darwin-10.9-amd64", newVersion) 599 | case "linux": 600 | oldVersionBinary = fmt.Sprintf("cockroach-%s.linux-amd64", oldVersion) 601 | newVersionBinary = fmt.Sprintf("cockroach-%s.linux-amd64", newVersion) 602 | default: 603 | t.Fatalf("unsupported os for test: %s", runtime.GOOS) 604 | } 605 | 606 | getBinary := func(fileName string) { 607 | require.NoError(t, exec.Command("mkdir", "./temp_binaries").Start()) 608 | require.NoError(t, downloadBinaryTest(fmt.Sprintf("./temp_binaries/%s.tgz", fileName), 609 | fmt.Sprintf("https://binaries.cockroachdb.com/%s.tgz", fileName))) 610 | tarCmd := exec.Command("tar", "-zxvf", fmt.Sprintf("./temp_binaries/%s.tgz", fileName), "-C", "./temp_binaries") 611 | require.NoError(t, tarCmd.Start()) 612 | require.NoError(t, tarCmd.Wait()) 613 | } 614 | 615 | defer func() { 616 | require.NoError(t, exec.Command("rm", "-rf", "./temp_binaries").Run()) 617 | }() 618 | 619 | getBinary(oldVersionBinary) 620 | getBinary(newVersionBinary) 621 | 622 | absPathOldBinary, err := filepath.Abs(fmt.Sprintf("./temp_binaries/%s/cockroach", oldVersionBinary)) 623 | require.NoError(t, err) 624 | 625 | absPathNewBinary, err := filepath.Abs(fmt.Sprintf("./temp_binaries/%s/cockroach", newVersionBinary)) 626 | require.NoError(t, err) 627 | 628 | ts, err := testserver.NewTestServer( 629 | testserver.ThreeNodeOpt(), 630 | testserver.CockroachBinaryPathOpt(absPathOldBinary), 631 | testserver.UpgradeCockroachBinaryPathOpt(absPathNewBinary), 632 | testserver.StoreOnDiskOpt(), 633 | ) 634 | require.NoError(t, err) 635 | defer ts.Stop() 636 | 637 | for i := 0; i < 3; i++ { 638 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 639 | } 640 | 641 | url := ts.PGURL() 642 | db, err := sql.Open("postgres", url.String()) 643 | require.NoError(t, err) 644 | defer func() { 645 | require.NoError(t, db.Close()) 646 | }() 647 | 648 | var version string 649 | row := db.QueryRow("SHOW CLUSTER SETTING version") 650 | err = row.Scan(&version) 651 | require.NoError(t, err) 652 | 653 | _, err = db.Exec("SET CLUSTER SETTING cluster.preserve_downgrade_option = '21.2';") 654 | require.NoError(t, err) 655 | require.NoError(t, db.Close()) 656 | 657 | for i := 0; i < 3; i++ { 658 | require.NoError(t, ts.UpgradeNode(i)) 659 | require.NoError(t, ts.WaitForInitFinishForNode(i)) 660 | } 661 | 662 | for i := 0; i < 3; i++ { 663 | url := ts.PGURLForNode(i) 664 | 665 | db, err = sql.Open("postgres", url.String()) 666 | require.NoError(t, err) 667 | 668 | var out int 669 | row = db.QueryRow("SELECT 1") 670 | err = row.Scan(&out) 671 | require.NoError(t, err) 672 | require.NoError(t, db.Close()) 673 | } 674 | 675 | db, err = sql.Open("postgres", ts.PGURL().String()) 676 | require.NoError(t, err) 677 | defer db.Close() 678 | _, err = db.Exec("RESET CLUSTER SETTING cluster.preserve_downgrade_option;") 679 | require.NoError(t, err) 680 | 681 | updated := false 682 | for start := time.Now(); time.Since(start) < 300*time.Second; { 683 | row = db.QueryRow("SHOW CLUSTER SETTING version") 684 | err = row.Scan(&version) 685 | if err != nil { 686 | t.Fatal(err) 687 | } 688 | if version == "22.1" { 689 | updated = true 690 | break 691 | } 692 | time.Sleep(time.Second) 693 | } 694 | 695 | if !updated { 696 | t.Fatal("update to 22.1 did not complete") 697 | } 698 | } 699 | 700 | // testFlockWithDownloadPassing is to test the flock over downloaded CRDB binary with 701 | // two goroutines, the second goroutine waits for the first goroutine to 702 | // finish downloading the CRDB binary into a local file. 703 | func testFlockWithDownloadPassing( 704 | t *testing.T, opts ...testserver.TestServerOpt, 705 | ) (*sql.DB, func()) { 706 | var wg = sync.WaitGroup{} 707 | 708 | localFile, err := getLocalFile(false) 709 | if err != nil { 710 | t.Fatal(err) 711 | } 712 | 713 | // Remove existing local file, to ensure that the first goroutine will download the CRDB binary. 714 | if err := removeExistingLocalFile(localFile); err != nil { 715 | t.Fatal(err) 716 | } 717 | 718 | wg.Add(2) 719 | var db *sql.DB 720 | var stop func() 721 | 722 | // First NewDBForTest goroutine to download the CRDB binary to the local file. 723 | go func() { 724 | db, stop = testserver.NewDBForTest(t, opts...) 725 | wg.Done() 726 | }() 727 | // Wait for 2 seconds, and then start the second NewDBForTest process. 728 | time.Sleep(2000 * time.Millisecond) 729 | // The second goroutine needs to wait until the first goroutine finishes downloading. 730 | // It will print "waiting for download of ..." in log for about 30 seconds. 731 | go func() { 732 | db, stop = testserver.NewDBForTest(t, opts...) 733 | wg.Done() 734 | }() 735 | wg.Wait() 736 | return db, stop 737 | } 738 | 739 | // testFlockWithDownloadKilled is to simulate the case that a NewDBForTest process is 740 | // killed before finishing downloading CRDB binary, and another NewDBForTest process has 741 | // to remove the existing local file and redownload. 742 | func testFlockWithDownloadKilled(t *testing.T) (*sql.DB, func()) { 743 | localFile, err := getLocalFile(false) 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | 748 | // Remove existing local file, to ensure that the first goroutine will download the CRDB binary. 749 | if err := removeExistingLocalFile(localFile); err != nil { 750 | t.Fatal(err) 751 | } 752 | 753 | // First NewDBForTest process to download the CRDB binary to the local file, 754 | // but killed in the middle. 755 | _, stop := testserver.NewDBForTest(t, testserver.StopDownloadInMiddleOpt()) 756 | stop() 757 | // Start the second NewDBForTest process. 758 | // It will remove the local file and redownload. 759 | return testserver.NewDBForTest(t) 760 | 761 | } 762 | 763 | // getLocalFile returns the to-be-downloaded CRDB binary's local path. 764 | func getLocalFile(nonStable bool) (string, error) { 765 | _, latestStableVersion, err := testserver.GetDownloadURL("", nonStable) 766 | if err != nil { 767 | return "", err 768 | } 769 | filename, err := testserver.GetDownloadFilename(latestStableVersion) 770 | if err != nil { 771 | return "", err 772 | } 773 | localFile := filepath.Join(os.TempDir(), filename) 774 | return localFile, err 775 | } 776 | 777 | // removeExistingLocalFile removes the existing local file for downloaded CRDB binary. 778 | func removeExistingLocalFile(localFile string) error { 779 | _, err := os.Stat(localFile) 780 | if err == nil { 781 | if err := os.Remove(localFile); err != nil { 782 | return fmt.Errorf("cannot remove local file: %s", err) 783 | } 784 | } 785 | return nil 786 | } 787 | 788 | func TestLocalityFlagsOpt(t *testing.T) { 789 | ts, err := testserver.NewTestServer( 790 | testserver.ThreeNodeOpt(), 791 | testserver.LocalityFlagsOpt("region=us-east1", "region=us-central1", "region=us-west1")) 792 | require.NoError(t, err) 793 | 794 | for i := 0; i < 3; i++ { 795 | ts.WaitForInitFinishForNode(i) 796 | } 797 | 798 | db, err := sql.Open("postgres", ts.PGURL().String()) 799 | require.NoError(t, err) 800 | 801 | found := map[string]bool{} 802 | 803 | rows, err := db.Query("SELECT region FROM [SHOW REGIONS]") 804 | require.NoError(t, err) 805 | defer rows.Close() 806 | for rows.Next() { 807 | var region string 808 | require.NoError(t, rows.Scan(®ion)) 809 | found[region] = true 810 | } 811 | 812 | require.Equal(t, map[string]bool{ 813 | "us-east1": true, 814 | "us-central1": true, 815 | "us-west1": true, 816 | }, found) 817 | } 818 | 819 | func TestCockroachLogsDirOpt(t *testing.T) { 820 | logsDir, err := os.MkdirTemp("", "logs-dir-opt") 821 | require.NoError(t, err) 822 | defer require.NoError(t, os.RemoveAll(logsDir)) 823 | 824 | ts, err := testserver.NewTestServer( 825 | testserver.ThreeNodeOpt(), 826 | testserver.CockroachLogsDirOpt(logsDir)) 827 | require.NoError(t, err) 828 | 829 | for i := 0; i < 3; i++ { 830 | if err := ts.WaitForInitFinishForNode(i); err != nil { 831 | // Make sure we stop the testserver in this case as well. 832 | ts.Stop() 833 | require.NoError(t, err) 834 | } 835 | } 836 | 837 | // This should delete all resources, but log files should 838 | // continue to exist under `logsDir`. 839 | ts.Stop() 840 | 841 | for _, nodeID := range []string{"0", "1", "2"} { 842 | for _, logFile := range []string{"cockroach.stdout", "cockroach.stderr"} { 843 | _, err := os.Stat(filepath.Join(logsDir, nodeID, logFile)) 844 | require.NoError(t, err) 845 | } 846 | } 847 | } 848 | -------------------------------------------------------------------------------- /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 | func (ts *testServerImpl) StartNode(i int) error { 55 | ts.mu.RLock() 56 | if ts.nodes[i].state == stateRunning { 57 | return fmt.Errorf("node %d already running", i) 58 | } 59 | ts.mu.RUnlock() 60 | 61 | // We need to compute the join addresses here. since if the listen port is 62 | // 0, then the actual port will not be known until a node is started. 63 | var joinAddrs []string 64 | for otherNodeID := range ts.nodes { 65 | if i == otherNodeID { 66 | continue 67 | } 68 | if ts.serverArgs.listenAddrPorts[otherNodeID] != 0 { 69 | joinAddrs = append(joinAddrs, fmt.Sprintf("localhost:%d", ts.serverArgs.listenAddrPorts[otherNodeID])) 70 | continue 71 | } 72 | select { 73 | case <-ts.pgURL[otherNodeID].started: 74 | // PGURLForNode will block until the URL is ready. If something 75 | // goes wrong, the goroutine waiting on pollListeningURLFile 76 | // will time out. 77 | joinAddrs = append(joinAddrs, fmt.Sprintf("localhost:%s", ts.PGURLForNode(otherNodeID).Port())) 78 | default: 79 | // If the other node hasn't started yet, don't add the join arg. 80 | } 81 | } 82 | joinArg := fmt.Sprintf("--join=%s", strings.Join(joinAddrs, ",")) 83 | 84 | args := ts.nodes[i].startCmdArgs 85 | if len(ts.nodes) > 1 { 86 | if len(joinAddrs) == 0 { 87 | // The start command always requires a --join arg, so we fake one 88 | // if we don't have any yet. 89 | joinArg = "--join=localhost:0" 90 | } 91 | args = append(args, joinArg) 92 | } 93 | ts.nodes[i].startCmd = exec.Command(args[0], args[1:]...) 94 | 95 | currCmd := ts.nodes[i].startCmd 96 | currCmd.Env = []string{ 97 | "COCKROACH_MAX_OFFSET=1ns", 98 | "COCKROACH_TRUST_CLIENT_PROVIDED_SQL_REMOTE_ADDR=true", 99 | } 100 | currCmd.Env = append(currCmd.Env, ts.serverArgs.envVars...) 101 | 102 | // Set the working directory of the cockroach process to our temp folder. 103 | // This stops cockroach from polluting the project directory with _dump 104 | // folders. 105 | currCmd.Dir = ts.baseDir 106 | 107 | if len(ts.nodes[i].stdout) > 0 { 108 | wr, err := newFileLogWriter(ts.nodes[i].stdout) 109 | if err != nil { 110 | return fmt.Errorf("unable to open file %s: %w", ts.nodes[i].stdout, err) 111 | } 112 | ts.nodes[i].stdoutBuf = wr 113 | } 114 | currCmd.Stdout = ts.nodes[i].stdoutBuf 115 | 116 | if len(ts.nodes[i].stderr) > 0 { 117 | wr, err := newFileLogWriter(ts.nodes[i].stderr) 118 | if err != nil { 119 | return fmt.Errorf("unable to open file %s: %w", ts.nodes[1].stderr, err) 120 | } 121 | ts.nodes[i].stderrBuf = wr 122 | } 123 | currCmd.Stderr = ts.nodes[i].stderrBuf 124 | 125 | for k, v := range defaultEnv() { 126 | currCmd.Env = append(currCmd.Env, k+"="+v) 127 | } 128 | 129 | log.Printf("executing: %s", currCmd) 130 | err := currCmd.Start() 131 | close(ts.pgURL[i].started) 132 | if currCmd.Process != nil { 133 | log.Printf("process %d started. env=%s; cmd: %s", currCmd.Process.Pid, currCmd.Env, strings.Join(args, " ")) 134 | } 135 | if err != nil { 136 | log.Print(err.Error()) 137 | ts.mu.Lock() 138 | ts.nodes[i].state = stateFailed 139 | ts.mu.Unlock() 140 | 141 | return fmt.Errorf("command %s failed: %w", currCmd, err) 142 | } 143 | 144 | ts.mu.Lock() 145 | ts.nodes[i].state = stateRunning 146 | ts.mu.Unlock() 147 | 148 | capturedI := i 149 | 150 | if ts.pgURL[capturedI].u == nil { 151 | if err := ts.pollListeningURLFile(capturedI); err != nil { 152 | log.Printf("%s failed to poll listening URL file: %v", testserverMessagePrefix, err) 153 | close(ts.pgURL[capturedI].set) 154 | ts.Stop() 155 | return err 156 | } 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (ts *testServerImpl) UpgradeNode(nodeNum int) error { 163 | err := ts.StopNode(nodeNum) 164 | if err != nil { 165 | return err 166 | } 167 | ts.nodes[nodeNum].startCmdArgs[0] = ts.serverArgs.upgradeCockroachBinary 168 | return ts.StartNode(nodeNum) 169 | } 170 | -------------------------------------------------------------------------------- /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 | // "v1.2.3-beta+md" 62 | func (v Version) String() string { 63 | var b bytes.Buffer 64 | fmt.Fprintf(&b, "v%d.%d.%d", v.major, v.minor, v.patch) 65 | if v.preRelease != "" { 66 | fmt.Fprintf(&b, "-%s", v.preRelease) 67 | } 68 | if v.metadata != "" { 69 | fmt.Fprintf(&b, "+%s", v.metadata) 70 | } 71 | return b.String() 72 | } 73 | 74 | // versionRE is the regexp that is used to verify that a version string is 75 | // of the form "vMAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]". This 76 | // conforms to https://semver.org/spec/v2.0.0.html 77 | var versionRE = regexp.MustCompile( 78 | `^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-.]+)?(\+[0-9A-Za-z-.]+|)?$`, 79 | // ^major ^minor ^patch ^preRelease ^metadata 80 | ) 81 | 82 | // numericRE is the regexp used to check if an identifier is numeric. 83 | var numericRE = regexp.MustCompile(`^(0|[1-9][0-9]*)$`) 84 | 85 | // Parse creates a version from a string. The string must be a valid semantic 86 | // version (as per https://semver.org/spec/v2.0.0.html) in the format: 87 | // "vMINOR.MAJOR.PATCH[-PRERELEASE][+METADATA]". 88 | // MINOR, MAJOR, and PATCH are numeric values (without any leading 0s). 89 | // PRERELEASE and METADATA can contain ASCII characters and digits, hyphens and 90 | // dots. 91 | func Parse(str string) (*Version, error) { 92 | if !versionRE.MatchString(str) { 93 | return nil, fmt.Errorf("invalid version string '%s'", str) 94 | } 95 | 96 | var v Version 97 | r := strings.NewReader(str) 98 | // Read the major.minor.patch part. 99 | _, err := fmt.Fscanf(r, "v%d.%d.%d", &v.major, &v.minor, &v.patch) 100 | if err != nil { 101 | panic(fmt.Sprintf("invalid version '%s' passed the regex: %s", str, err)) 102 | } 103 | remaining := str[len(str)-r.Len():] 104 | // Read the pre-release, if present. 105 | if len(remaining) > 0 && remaining[0] == '-' { 106 | p := strings.IndexRune(remaining, '+') 107 | if p == -1 { 108 | p = len(remaining) 109 | } 110 | v.preRelease = remaining[1:p] 111 | remaining = remaining[p:] 112 | } 113 | // Read the metadata, if present. 114 | if len(remaining) > 0 { 115 | if remaining[0] != '+' { 116 | panic(fmt.Sprintf("invalid version '%s' passed the regex", str)) 117 | } 118 | v.metadata = remaining[1:] 119 | } 120 | return &v, nil 121 | } 122 | 123 | // MustParse is like Parse but panics on any error. Recommended as an 124 | // initializer for global values. 125 | func MustParse(str string) *Version { 126 | v, err := Parse(str) 127 | if err != nil { 128 | panic(err) 129 | } 130 | return v 131 | } 132 | 133 | func cmpVal(a, b int32) int { 134 | if a > b { 135 | return +1 136 | } 137 | if a < b { 138 | return -1 139 | } 140 | return 0 141 | } 142 | 143 | // Compare returns -1, 0, or +1 indicating the relative ordering of versions. 144 | func (v *Version) Compare(w *Version) int { 145 | if v := cmpVal(v.major, w.major); v != 0 { 146 | return v 147 | } 148 | if v := cmpVal(v.minor, w.minor); v != 0 { 149 | return v 150 | } 151 | if v := cmpVal(v.patch, w.patch); v != 0 { 152 | return v 153 | } 154 | if v.preRelease != w.preRelease { 155 | if v.preRelease == "" && w.preRelease != "" { 156 | // 1.0.0 is greater than 1.0.0-alpha. 157 | return 1 158 | } 159 | if v.preRelease != "" && w.preRelease == "" { 160 | // 1.0.0-alpha is less than 1.0.0. 161 | return -1 162 | } 163 | 164 | // Quoting from https://semver.org/spec/v2.0.0.html: 165 | // Precedence for two pre-release versions with the same major, minor, and 166 | // patch version MUST be determined by comparing each dot separated 167 | // identifier from left to right until a difference is found as follows: 168 | // (1) Identifiers consisting of only digits are compared numerically. 169 | // (2) identifiers with letters or hyphens are compared lexically in ASCII 170 | // sort order. 171 | // (3) Numeric identifiers always have lower precedence than non-numeric 172 | // identifiers. 173 | // (4) A larger set of pre-release fields has a higher precedence than a 174 | // smaller set, if all of the preceding identifiers are equal. 175 | // 176 | vs := strings.Split(v.preRelease, ".") 177 | ws := strings.Split(w.preRelease, ".") 178 | for ; len(vs) > 0 && len(ws) > 0; vs, ws = vs[1:], ws[1:] { 179 | vStr, wStr := vs[0], ws[0] 180 | if vStr == wStr { 181 | continue 182 | } 183 | vNumeric := numericRE.MatchString(vStr) 184 | wNumeric := numericRE.MatchString(wStr) 185 | switch { 186 | case vNumeric && wNumeric: 187 | // Case 1. 188 | vVal, err := strconv.Atoi(vStr) 189 | if err != nil { 190 | panic(err) 191 | } 192 | wVal, err := strconv.Atoi(wStr) 193 | if err != nil { 194 | panic(err) 195 | } 196 | if vVal == wVal { 197 | panic("different strings yield the same numbers") 198 | } 199 | if vVal < wVal { 200 | return -1 201 | } 202 | return 1 203 | 204 | case vNumeric: 205 | // Case 3. 206 | return -1 207 | 208 | case wNumeric: 209 | // Case 3. 210 | return 1 211 | 212 | default: 213 | // Case 2. 214 | if vStr < wStr { 215 | return -1 216 | } 217 | return 1 218 | } 219 | } 220 | 221 | if len(vs) > 0 { 222 | // Case 4. 223 | return +1 224 | } 225 | if len(ws) > 0 { 226 | // Case 4. 227 | return -1 228 | } 229 | } 230 | 231 | return 0 232 | } 233 | 234 | // AtLeast returns true if v >= w. 235 | func (v *Version) AtLeast(w *Version) bool { 236 | return v.Compare(w) >= 0 237 | } 238 | --------------------------------------------------------------------------------