github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/bank/bank.go (about) 1 // Copyright 2017 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 bank 12 13 import ( 14 "context" 15 gosql "database/sql" 16 "fmt" 17 "strings" 18 19 "github.com/cockroachdb/cockroach/pkg/col/coldata" 20 "github.com/cockroachdb/cockroach/pkg/sql/types" 21 "github.com/cockroachdb/cockroach/pkg/util/bufalloc" 22 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 23 "github.com/cockroachdb/cockroach/pkg/workload" 24 "github.com/cockroachdb/cockroach/pkg/workload/histogram" 25 "github.com/cockroachdb/errors" 26 "github.com/spf13/pflag" 27 "golang.org/x/exp/rand" 28 ) 29 30 const ( 31 bankSchema = `( 32 id INT PRIMARY KEY, 33 balance INT, 34 payload STRING, 35 FAMILY (id, balance, payload) 36 )` 37 38 defaultRows = 1000 39 defaultBatchSize = 1000 40 defaultPayloadBytes = 100 41 defaultRanges = 10 42 maxTransfer = 999 43 ) 44 45 type bank struct { 46 flags workload.Flags 47 connFlags *workload.ConnFlags 48 49 seed uint64 50 rows, batchSize int 51 payloadBytes, ranges int 52 } 53 54 func init() { 55 workload.Register(bankMeta) 56 } 57 58 var bankMeta = workload.Meta{ 59 Name: `bank`, 60 Description: `Bank models a set of accounts with currency balances`, 61 Version: `1.0.0`, 62 PublicFacing: true, 63 New: func() workload.Generator { 64 g := &bank{} 65 g.flags.FlagSet = pflag.NewFlagSet(`bank`, pflag.ContinueOnError) 66 g.flags.Meta = map[string]workload.FlagMeta{ 67 `batch-size`: {RuntimeOnly: true}, 68 } 69 g.flags.Uint64Var(&g.seed, `seed`, 1, `Key hash seed.`) 70 g.flags.IntVar(&g.rows, `rows`, defaultRows, `Initial number of accounts in bank table.`) 71 g.flags.IntVar(&g.batchSize, `batch-size`, defaultBatchSize, `Number of rows in each batch of initial data.`) 72 g.flags.IntVar(&g.payloadBytes, `payload-bytes`, defaultPayloadBytes, `Size of the payload field in each initial row.`) 73 g.flags.IntVar(&g.ranges, `ranges`, defaultRanges, `Initial number of ranges in bank table.`) 74 g.connFlags = workload.NewConnFlags(&g.flags) 75 return g 76 }, 77 } 78 79 // FromRows returns Bank testdata with the given number of rows and default 80 // payload size and range count. 81 func FromRows(rows int) workload.Generator { 82 return FromConfig(rows, 1, defaultPayloadBytes, defaultRanges) 83 } 84 85 // FromConfig returns a one table testdata with three columns: an `id INT 86 // PRIMARY KEY` representing an account number, a `balance` INT, and a `payload` 87 // BYTES to pad the size of the rows for various tests. 88 func FromConfig(rows int, batchSize int, payloadBytes int, ranges int) workload.Generator { 89 if ranges > rows { 90 ranges = rows 91 } 92 if batchSize <= 0 { 93 batchSize = defaultBatchSize 94 } 95 return workload.FromFlags(bankMeta, 96 fmt.Sprintf(`--rows=%d`, rows), 97 fmt.Sprintf(`--batch-size=%d`, batchSize), 98 fmt.Sprintf(`--payload-bytes=%d`, payloadBytes), 99 fmt.Sprintf(`--ranges=%d`, ranges), 100 ) 101 } 102 103 // Meta implements the Generator interface. 104 func (*bank) Meta() workload.Meta { return bankMeta } 105 106 // Flags implements the Flagser interface. 107 func (b *bank) Flags() workload.Flags { return b.flags } 108 109 // Hooks implements the Hookser interface. 110 func (b *bank) Hooks() workload.Hooks { 111 return workload.Hooks{ 112 Validate: func() error { 113 if b.rows < b.ranges { 114 return errors.Errorf( 115 "Value of 'rows' (%d) must be greater than or equal to value of 'ranges' (%d)", 116 b.rows, b.ranges) 117 } 118 if b.batchSize <= 0 { 119 return errors.Errorf(`Value of batch-size must be greater than zero; was %d`, b.batchSize) 120 } 121 return nil 122 }, 123 } 124 } 125 126 var bankTypes = []*types.T{ 127 types.Int, 128 types.Int, 129 types.Bytes, 130 } 131 132 // Tables implements the Generator interface. 133 func (b *bank) Tables() []workload.Table { 134 numBatches := (b.rows + b.batchSize - 1) / b.batchSize // ceil(b.rows/b.batchSize) 135 table := workload.Table{ 136 Name: `bank`, 137 Schema: bankSchema, 138 InitialRows: workload.BatchedTuples{ 139 NumBatches: numBatches, 140 FillBatch: func(batchIdx int, cb coldata.Batch, a *bufalloc.ByteAllocator) { 141 rng := rand.NewSource(b.seed + uint64(batchIdx)) 142 143 rowBegin, rowEnd := batchIdx*b.batchSize, (batchIdx+1)*b.batchSize 144 if rowEnd > b.rows { 145 rowEnd = b.rows 146 } 147 cb.Reset(bankTypes, rowEnd-rowBegin, coldata.StandardColumnFactory) 148 idCol := cb.ColVec(0).Int64() 149 balanceCol := cb.ColVec(1).Int64() 150 payloadCol := cb.ColVec(2).Bytes() 151 // coldata.Bytes only allows appends so we have to reset it 152 payloadCol.Reset() 153 for rowIdx := rowBegin; rowIdx < rowEnd; rowIdx++ { 154 var payload []byte 155 *a, payload = a.Alloc(b.payloadBytes, 0 /* extraCap */) 156 const initialPrefix = `initial-` 157 copy(payload[:len(initialPrefix)], []byte(initialPrefix)) 158 randStringLetters(rng, payload[len(initialPrefix):]) 159 160 rowOffset := rowIdx - rowBegin 161 idCol[rowOffset] = int64(rowIdx) 162 balanceCol[rowOffset] = 0 163 payloadCol.Set(rowOffset, payload) 164 } 165 }, 166 }, 167 Splits: workload.Tuples( 168 b.ranges-1, 169 func(splitIdx int) []interface{} { 170 return []interface{}{ 171 (splitIdx + 1) * (b.rows / b.ranges), 172 } 173 }, 174 ), 175 } 176 return []workload.Table{table} 177 } 178 179 // Ops implements the Opser interface. 180 func (b *bank) Ops(urls []string, reg *histogram.Registry) (workload.QueryLoad, error) { 181 sqlDatabase, err := workload.SanitizeUrls(b, b.connFlags.DBOverride, urls) 182 if err != nil { 183 return workload.QueryLoad{}, err 184 } 185 db, err := gosql.Open(`cockroach`, strings.Join(urls, ` `)) 186 if err != nil { 187 return workload.QueryLoad{}, err 188 } 189 // Allow a maximum of concurrency+1 connections to the database. 190 db.SetMaxOpenConns(b.connFlags.Concurrency + 1) 191 db.SetMaxIdleConns(b.connFlags.Concurrency + 1) 192 193 // TODO(dan): Move the various queries in the backup/restore tests here. 194 updateStmt, err := db.Prepare(` 195 UPDATE bank 196 SET balance = CASE id WHEN $1 THEN balance-$3 WHEN $2 THEN balance+$3 END 197 WHERE id IN ($1, $2) 198 `) 199 if err != nil { 200 return workload.QueryLoad{}, err 201 } 202 203 ql := workload.QueryLoad{SQLDatabase: sqlDatabase} 204 for i := 0; i < b.connFlags.Concurrency; i++ { 205 rng := rand.New(rand.NewSource(b.seed)) 206 hists := reg.GetHandle() 207 workerFn := func(ctx context.Context) error { 208 from := rng.Intn(b.rows) 209 to := rng.Intn(b.rows - 1) 210 for from == to && b.rows != 1 { 211 to = rng.Intn(b.rows - 1) 212 } 213 amount := rand.Intn(maxTransfer) 214 start := timeutil.Now() 215 _, err := updateStmt.Exec(from, to, amount) 216 elapsed := timeutil.Since(start) 217 hists.Get(`transfer`).Record(elapsed) 218 return err 219 } 220 ql.WorkerFns = append(ql.WorkerFns, workerFn) 221 } 222 return ql, nil 223 } 224 225 // NOTE: The following is intentionally duplicated with the ones in 226 // workload/tpcc/generate.go. They're a very hot path in restoring a fixture and 227 // hardcoding the consts seems to trigger some compiler optimizations that don't 228 // happen if those things are params. Don't modify these without consulting 229 // BenchmarkRandStringFast. 230 231 func randStringLetters(rng rand.Source, buf []byte) { 232 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 233 const lettersLen = uint64(len(letters)) 234 const lettersCharsPerRand = uint64(11) // floor(log(math.MaxUint64)/log(lettersLen)) 235 236 var r, charsLeft uint64 237 for i := 0; i < len(buf); i++ { 238 if charsLeft == 0 { 239 r = rng.Uint64() 240 charsLeft = lettersCharsPerRand 241 } 242 buf[i] = letters[r%lettersLen] 243 r = r / lettersLen 244 charsLeft-- 245 } 246 }