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  }