github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/ycsb/ycsb.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 ycsb is the workload specified by the Yahoo! Cloud Serving Benchmark.
    12  package ycsb
    13  
    14  import (
    15  	"context"
    16  	gosql "database/sql"
    17  	"encoding/binary"
    18  	"fmt"
    19  	"hash"
    20  	"hash/fnv"
    21  	"math"
    22  	"math/rand"
    23  	"strings"
    24  	"sync/atomic"
    25  
    26  	"github.com/cockroachdb/cockroach-go/crdb"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    28  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    29  	"github.com/cockroachdb/cockroach/pkg/workload"
    30  	"github.com/cockroachdb/cockroach/pkg/workload/histogram"
    31  	"github.com/cockroachdb/errors"
    32  	"github.com/spf13/pflag"
    33  )
    34  
    35  const (
    36  	numTableFields = 10
    37  	fieldLength    = 100 // In characters
    38  	zipfIMin       = 0
    39  
    40  	usertableSchemaRelational = `(
    41  		ycsb_key VARCHAR(255) PRIMARY KEY NOT NULL,
    42  		FIELD0 TEXT NOT NULL,
    43  		FIELD1 TEXT NOT NULL,
    44  		FIELD2 TEXT NOT NULL,
    45  		FIELD3 TEXT NOT NULL,
    46  		FIELD4 TEXT NOT NULL,
    47  		FIELD5 TEXT NOT NULL,
    48  		FIELD6 TEXT NOT NULL,
    49  		FIELD7 TEXT NOT NULL,
    50  		FIELD8 TEXT NOT NULL,
    51  		FIELD9 TEXT NOT NULL
    52  	)`
    53  	usertableSchemaRelationalWithFamilies = `(
    54  		ycsb_key VARCHAR(255) PRIMARY KEY NOT NULL,
    55  		FIELD0 TEXT NOT NULL,
    56  		FIELD1 TEXT NOT NULL,
    57  		FIELD2 TEXT NOT NULL,
    58  		FIELD3 TEXT NOT NULL,
    59  		FIELD4 TEXT NOT NULL,
    60  		FIELD5 TEXT NOT NULL,
    61  		FIELD6 TEXT NOT NULL,
    62  		FIELD7 TEXT NOT NULL,
    63  		FIELD8 TEXT NOT NULL,
    64  		FIELD9 TEXT NOT NULL,
    65  		FAMILY (ycsb_key),
    66  		FAMILY (FIELD0),
    67  		FAMILY (FIELD1),
    68  		FAMILY (FIELD2),
    69  		FAMILY (FIELD3),
    70  		FAMILY (FIELD4),
    71  		FAMILY (FIELD5),
    72  		FAMILY (FIELD6),
    73  		FAMILY (FIELD7),
    74  		FAMILY (FIELD8),
    75  		FAMILY (FIELD9)
    76  	)`
    77  	usertableSchemaJSON = `(
    78  		ycsb_key VARCHAR(255) PRIMARY KEY NOT NULL,
    79  		FIELD JSONB
    80  	)`
    81  )
    82  
    83  type ycsb struct {
    84  	flags     workload.Flags
    85  	connFlags *workload.ConnFlags
    86  
    87  	seed        int64
    88  	insertStart int
    89  	insertCount int
    90  	recordCount int
    91  	json        bool
    92  	families    bool
    93  	sfu         bool
    94  	splits      int
    95  
    96  	workload                                                        string
    97  	requestDistribution                                             string
    98  	scanLengthDistribution                                          string
    99  	minScanLength, maxScanLength                                    uint64
   100  	readFreq, insertFreq, updateFreq, scanFreq, readModifyWriteFreq float32
   101  }
   102  
   103  func init() {
   104  	workload.Register(ycsbMeta)
   105  }
   106  
   107  var ycsbMeta = workload.Meta{
   108  	Name:         `ycsb`,
   109  	Description:  `YCSB is the Yahoo! Cloud Serving Benchmark`,
   110  	Version:      `1.0.0`,
   111  	PublicFacing: true,
   112  	New: func() workload.Generator {
   113  		g := &ycsb{}
   114  		g.flags.FlagSet = pflag.NewFlagSet(`ycsb`, pflag.ContinueOnError)
   115  		g.flags.Meta = map[string]workload.FlagMeta{
   116  			`workload`: {RuntimeOnly: true},
   117  		}
   118  		g.flags.Int64Var(&g.seed, `seed`, 1, `Key hash seed.`)
   119  		g.flags.IntVar(&g.insertStart, `insert-start`, 0, `Key to start initial sequential insertions from. (default 0)`)
   120  		g.flags.IntVar(&g.insertCount, `insert-count`, 10000, `Number of rows to sequentially insert before beginning workload.`)
   121  		g.flags.IntVar(&g.recordCount, `record-count`, 0, `Key to start workload insertions from. Must be >= insert-start + insert-count. (Default: insert-start + insert-count)`)
   122  		g.flags.BoolVar(&g.json, `json`, false, `Use JSONB rather than relational data`)
   123  		g.flags.BoolVar(&g.families, `families`, true, `Place each column in its own column family`)
   124  		g.flags.BoolVar(&g.sfu, `select-for-update`, true, `Use SELECT FOR UPDATE syntax in read-modify-write transactions`)
   125  		g.flags.IntVar(&g.splits, `splits`, 0, `Number of splits to perform before starting normal operations`)
   126  		g.flags.StringVar(&g.workload, `workload`, `B`, `Workload type. Choose from A-F.`)
   127  		g.flags.StringVar(&g.requestDistribution, `request-distribution`, ``, `Distribution for request key generation [zipfian, uniform, latest]. The default for workloads A, B, C, E, and F is zipfian, and the default for workload D is latest.`)
   128  		g.flags.StringVar(&g.scanLengthDistribution, `scan-length-distribution`, `uniform`, `Distribution for scan length generation [zipfian, uniform]. Primarily used for workload E.`)
   129  		g.flags.Uint64Var(&g.minScanLength, `min-scan-length`, 1, `The minimum length for scan operations. Primarily used for workload E.`)
   130  		g.flags.Uint64Var(&g.maxScanLength, `max-scan-length`, 1000, `The maximum length for scan operations. Primarily used for workload E.`)
   131  
   132  		// TODO(dan): g.flags.Uint64Var(&g.maxWrites, `max-writes`,
   133  		//     7*24*3600*1500,  // 7 days at 5% writes and 30k ops/s
   134  		//     `Maximum number of writes to perform before halting. This is required for `+
   135  		// 	   `accurately generating keys that are uniformly distributed across the keyspace.`)
   136  		g.connFlags = workload.NewConnFlags(&g.flags)
   137  		return g
   138  	},
   139  }
   140  
   141  // Meta implements the Generator interface.
   142  func (*ycsb) Meta() workload.Meta { return ycsbMeta }
   143  
   144  // Flags implements the Flagser interface.
   145  func (g *ycsb) Flags() workload.Flags { return g.flags }
   146  
   147  // Hooks implements the Hookser interface.
   148  func (g *ycsb) Hooks() workload.Hooks {
   149  	return workload.Hooks{
   150  		Validate: func() error {
   151  			g.workload = strings.ToUpper(g.workload)
   152  			switch g.workload {
   153  			case "A":
   154  				g.readFreq = 0.5
   155  				g.updateFreq = 0.5
   156  				g.requestDistribution = "zipfian"
   157  			case "B":
   158  				g.readFreq = 0.95
   159  				g.updateFreq = 0.05
   160  				g.requestDistribution = "zipfian"
   161  			case "C":
   162  				g.readFreq = 1.0
   163  				g.requestDistribution = "zipfian"
   164  			case "D":
   165  				g.readFreq = 0.95
   166  				g.insertFreq = 0.05
   167  				g.requestDistribution = "latest"
   168  			case "E":
   169  				g.scanFreq = 0.95
   170  				g.insertFreq = 0.05
   171  				g.requestDistribution = "zipfian"
   172  			case "F":
   173  				g.readFreq = 0.5
   174  				g.readModifyWriteFreq = 0.5
   175  				g.requestDistribution = "zipfian"
   176  			default:
   177  				return errors.Errorf("Unknown workload: %q", g.workload)
   178  			}
   179  
   180  			if !g.flags.Lookup(`families`).Changed {
   181  				// If `--families` was not specified, default its value to the
   182  				// configuration that we expect to lead to better performance.
   183  				g.families = preferColumnFamilies(g.workload)
   184  			}
   185  
   186  			if g.recordCount == 0 {
   187  				g.recordCount = g.insertStart + g.insertCount
   188  			}
   189  			if g.insertStart+g.insertCount > g.recordCount {
   190  				return errors.Errorf("insertStart + insertCount (%d) must be <= recordCount (%d)", g.insertStart+g.insertCount, g.recordCount)
   191  			}
   192  			return nil
   193  		},
   194  	}
   195  }
   196  
   197  // preferColumnFamilies returns whether we expect the use of column families to
   198  // improve performance for a given workload.
   199  func preferColumnFamilies(workload string) bool {
   200  	// These determinations were computed on 80da27b (04/04/2020) while running
   201  	// the ycsb roachtests.
   202  	//
   203  	// ycsb/[A-F]/nodes=3 (3x n1-standard-8 VMs):
   204  	//
   205  	// | workload | --families=false | --families=true | better with families? |
   206  	// |----------|-----------------:|----------------:|-----------------------|
   207  	// | A        |         11,743.5 |        17,760.5 | true                  |
   208  	// | B        |         35,232.3 |        32,982.2 | false                 |
   209  	// | C        |         45,454.7 |        44,112.5 | false                 |
   210  	// | D        |         36,091.0 |        35,615.1 | false                 |
   211  	// | E        |          5,774.9 |         2,604.8 | false                 |
   212  	// | F        |          4,933.1 |         8,259.7 | true                  |
   213  	//
   214  	// ycsb/[A-F]/nodes=3/cpu=32 (3x n1-standard-32 VMs):
   215  	//
   216  	// | workload | --families=false | --families=true | better with families? |
   217  	// |----------|-----------------:|----------------:|-----------------------|
   218  	// | A        |         14,144.1 |        27,179.4 | true                  |
   219  	// | B        |         96,669.6 |       104,567.5 | true                  |
   220  	// | C        |        137,463.3 |       131,953.7 | false                 |
   221  	// | D        |        103,188.6 |        95,285.7 | false                 |
   222  	// | E        |         10,417.5 |         7,913.6 | false                 |
   223  	// | F        |          5,782.3 |        15,532.1 | true                  |
   224  	//
   225  	switch workload {
   226  	case "A":
   227  		// Workload A is highly contended. It performs 50% single-row lookups
   228  		// and 50% single-column updates. Using column families breaks the
   229  		// contention between all updates to different columns of the same row,
   230  		// so we use them by default.
   231  		return true
   232  	case "B":
   233  		// Workload B is less contended than Workload A, but still bottlenecks
   234  		// on contention as concurrency grows. It performs 95% single-row
   235  		// lookups and 5% single-column updates. Using column families slows
   236  		// down the single-row lookups but speeds up the updates (see above).
   237  		// This trade-off favors column families for higher concurrency levels
   238  		// but does not at lower concurrency levels. We prefer larger YCSB
   239  		// deployments, so we use column families by default.
   240  		return true
   241  	case "C":
   242  		// Workload C has no contention. It consistent entirely of single-row
   243  		// lookups. Using column families slows down single-row lookups, so we
   244  		// do not use them by default.
   245  		return false
   246  	case "D":
   247  		// Workload D has no contention. It performs 95% single-row lookups and
   248  		// 5% single-row insertion. Using column families slows down single-row
   249  		// lookups and single-row insertion, so we do not use them by default.
   250  		return false
   251  	case "E":
   252  		// Workload E has moderate contention. It performs 95% multi-row scans
   253  		// and 5% single-row insertion. Using column families slows down
   254  		// multi-row scans and single-row insertion, so we do not use them by
   255  		// default.
   256  		return false
   257  	case "F":
   258  		// Workload F is highly contended. It performs 50% single-row lookups
   259  		// and 50% single-column updates expressed as multi-statement
   260  		// read-modify-write transactions. Using column families breaks the
   261  		// contention between all updates to different columns of the same row,
   262  		// so we use them by default.
   263  		return true
   264  	default:
   265  		panic(fmt.Sprintf("unexpected workload: %s", workload))
   266  	}
   267  }
   268  
   269  var usertableTypes = []*types.T{
   270  	types.Bytes, types.Bytes, types.Bytes, types.Bytes, types.Bytes, types.Bytes,
   271  	types.Bytes, types.Bytes, types.Bytes, types.Bytes, types.Bytes,
   272  }
   273  
   274  // Tables implements the Generator interface.
   275  func (g *ycsb) Tables() []workload.Table {
   276  	usertable := workload.Table{
   277  		Name: `usertable`,
   278  		Splits: workload.Tuples(
   279  			g.splits,
   280  			func(splitIdx int) []interface{} {
   281  				step := math.MaxUint64 / uint64(g.splits+1)
   282  				return []interface{}{
   283  					keyNameFromHash(step * uint64(splitIdx+1)),
   284  				}
   285  			},
   286  		),
   287  	}
   288  	usertableInitialRowsFn := func(rowIdx int) []interface{} {
   289  		w := ycsbWorker{config: g, hashFunc: fnv.New64()}
   290  		key := w.buildKeyName(uint64(g.insertStart + rowIdx))
   291  		if g.json {
   292  			return []interface{}{key, "{}"}
   293  		}
   294  		return []interface{}{key, "", "", "", "", "", "", "", "", "", ""}
   295  	}
   296  	if g.json {
   297  		usertable.Schema = usertableSchemaJSON
   298  		usertable.InitialRows = workload.Tuples(
   299  			g.insertCount,
   300  			usertableInitialRowsFn,
   301  		)
   302  	} else {
   303  		if g.families {
   304  			usertable.Schema = usertableSchemaRelationalWithFamilies
   305  		} else {
   306  			usertable.Schema = usertableSchemaRelational
   307  		}
   308  		usertable.InitialRows = workload.TypedTuples(
   309  			g.insertCount,
   310  			usertableTypes,
   311  			usertableInitialRowsFn,
   312  		)
   313  	}
   314  	return []workload.Table{usertable}
   315  }
   316  
   317  // Ops implements the Opser interface.
   318  func (g *ycsb) Ops(urls []string, reg *histogram.Registry) (workload.QueryLoad, error) {
   319  	sqlDatabase, err := workload.SanitizeUrls(g, g.connFlags.DBOverride, urls)
   320  	if err != nil {
   321  		return workload.QueryLoad{}, err
   322  	}
   323  	db, err := gosql.Open(`cockroach`, strings.Join(urls, ` `))
   324  	if err != nil {
   325  		return workload.QueryLoad{}, err
   326  	}
   327  	// Allow a maximum of concurrency+1 connections to the database.
   328  	db.SetMaxOpenConns(g.connFlags.Concurrency + 1)
   329  	db.SetMaxIdleConns(g.connFlags.Concurrency + 1)
   330  
   331  	readStmt, err := db.Prepare(`SELECT * FROM usertable WHERE ycsb_key = $1`)
   332  	if err != nil {
   333  		return workload.QueryLoad{}, err
   334  	}
   335  
   336  	readFieldForUpdateStmts := make([]*gosql.Stmt, numTableFields)
   337  	for i := 0; i < numTableFields; i++ {
   338  		var q string
   339  		if g.json {
   340  			q = fmt.Sprintf(`SELECT field->>'field%d' FROM usertable WHERE ycsb_key = $1`, i)
   341  		} else {
   342  			q = fmt.Sprintf(`SELECT field%d FROM usertable WHERE ycsb_key = $1`, i)
   343  		}
   344  		if g.sfu {
   345  			q = fmt.Sprintf(`%s FOR UPDATE`, q)
   346  		}
   347  
   348  		stmt, err := db.Prepare(q)
   349  		if err != nil {
   350  			return workload.QueryLoad{}, err
   351  		}
   352  		readFieldForUpdateStmts[i] = stmt
   353  	}
   354  
   355  	scanStmt, err := db.Prepare(`SELECT * FROM usertable WHERE ycsb_key >= $1 LIMIT $2`)
   356  	if err != nil {
   357  		return workload.QueryLoad{}, err
   358  	}
   359  
   360  	var insertStmt *gosql.Stmt
   361  	if g.json {
   362  		insertStmt, err = db.Prepare(`INSERT INTO usertable VALUES ($1, json_build_object(
   363  			'field0',  $2:::text,
   364  			'field1',  $3:::text,
   365  			'field2',  $4:::text,
   366  			'field3',  $5:::text,
   367  			'field4',  $6:::text,
   368  			'field5',  $7:::text,
   369  			'field6',  $8:::text,
   370  			'field7',  $9:::text,
   371  			'field8',  $10:::text,
   372  			'field9',  $11:::text
   373  		))`)
   374  	} else {
   375  		insertStmt, err = db.Prepare(`INSERT INTO usertable VALUES (
   376  			$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
   377  		)`)
   378  	}
   379  	if err != nil {
   380  		return workload.QueryLoad{}, err
   381  	}
   382  
   383  	updateStmts := make([]*gosql.Stmt, numTableFields)
   384  	if g.json {
   385  		stmt, err := db.Prepare(`UPDATE usertable SET field = field || $2 WHERE ycsb_key = $1`)
   386  		if err != nil {
   387  			return workload.QueryLoad{}, err
   388  		}
   389  		updateStmts[0] = stmt
   390  	} else {
   391  		for i := 0; i < numTableFields; i++ {
   392  			q := fmt.Sprintf(`UPDATE usertable SET field%d = $2 WHERE ycsb_key = $1`, i)
   393  			stmt, err := db.Prepare(q)
   394  			if err != nil {
   395  				return workload.QueryLoad{}, err
   396  			}
   397  			updateStmts[i] = stmt
   398  		}
   399  	}
   400  
   401  	rowIndexVal := uint64(g.recordCount)
   402  	rowIndex := &rowIndexVal
   403  	rowCounter := NewAcknowledgedCounter((uint64)(g.recordCount))
   404  
   405  	var requestGen randGenerator
   406  	requestGenRng := rand.New(rand.NewSource(g.seed))
   407  	switch strings.ToLower(g.requestDistribution) {
   408  	case "zipfian":
   409  		requestGen, err = NewZipfGenerator(
   410  			requestGenRng, zipfIMin, defaultIMax-1, defaultTheta, false /* verbose */)
   411  	case "uniform":
   412  		requestGen, err = NewUniformGenerator(requestGenRng, 0, uint64(g.recordCount)-1)
   413  	case "latest":
   414  		requestGen, err = NewSkewedLatestGenerator(
   415  			requestGenRng, zipfIMin, uint64(g.recordCount)-1, defaultTheta, false /* verbose */)
   416  	default:
   417  		return workload.QueryLoad{}, errors.Errorf("Unknown request distribution: %s", g.requestDistribution)
   418  	}
   419  	if err != nil {
   420  		return workload.QueryLoad{}, err
   421  	}
   422  
   423  	var scanLengthGen randGenerator
   424  	scanLengthGenRng := rand.New(rand.NewSource(g.seed + 1))
   425  	switch strings.ToLower(g.scanLengthDistribution) {
   426  	case "zipfian":
   427  		scanLengthGen, err = NewZipfGenerator(scanLengthGenRng, g.minScanLength, g.maxScanLength, defaultTheta, false /* verbose */)
   428  	case "uniform":
   429  		scanLengthGen, err = NewUniformGenerator(scanLengthGenRng, g.minScanLength, g.maxScanLength)
   430  	default:
   431  		return workload.QueryLoad{}, errors.Errorf("Unknown scan length distribution: %s", g.scanLengthDistribution)
   432  	}
   433  	if err != nil {
   434  		return workload.QueryLoad{}, err
   435  	}
   436  
   437  	ql := workload.QueryLoad{SQLDatabase: sqlDatabase}
   438  	for i := 0; i < g.connFlags.Concurrency; i++ {
   439  		rng := rand.New(rand.NewSource(g.seed + int64(i)))
   440  		w := &ycsbWorker{
   441  			config:                  g,
   442  			hists:                   reg.GetHandle(),
   443  			db:                      db,
   444  			readStmt:                readStmt,
   445  			readFieldForUpdateStmts: readFieldForUpdateStmts,
   446  			scanStmt:                scanStmt,
   447  			insertStmt:              insertStmt,
   448  			updateStmts:             updateStmts,
   449  			rowIndex:                rowIndex,
   450  			rowCounter:              rowCounter,
   451  			nextInsertIndex:         nil,
   452  			requestGen:              requestGen,
   453  			scanLengthGen:           scanLengthGen,
   454  			rng:                     rng,
   455  			hashFunc:                fnv.New64(),
   456  		}
   457  		ql.WorkerFns = append(ql.WorkerFns, w.run)
   458  	}
   459  	return ql, nil
   460  }
   461  
   462  type randGenerator interface {
   463  	Uint64() uint64
   464  	IncrementIMax(count uint64) error
   465  }
   466  
   467  type ycsbWorker struct {
   468  	config *ycsb
   469  	hists  *histogram.Histograms
   470  	db     *gosql.DB
   471  	// Statement to read all the fields of a row. Used for read requests.
   472  	readStmt *gosql.Stmt
   473  	// Statements to read a specific field of a row in preparation for
   474  	// updating it. Used for read-modify-write requests.
   475  	readFieldForUpdateStmts []*gosql.Stmt
   476  	scanStmt, insertStmt    *gosql.Stmt
   477  	// In normal mode this is one statement per field, since the field name
   478  	// cannot be parametrized. In JSON mode it's a single statement.
   479  	updateStmts []*gosql.Stmt
   480  
   481  	// The next row index to insert.
   482  	rowIndex *uint64
   483  	// Counter to keep track of which rows have been inserted.
   484  	rowCounter *AcknowledgedCounter
   485  	// Next insert index to use if non-nil.
   486  	nextInsertIndex *uint64
   487  
   488  	requestGen    randGenerator // used to generate random keys for requests
   489  	scanLengthGen randGenerator // used to generate length of scan operations
   490  	rng           *rand.Rand    // used to generate random strings for the values
   491  	hashFunc      hash.Hash64
   492  	hashBuf       [8]byte
   493  }
   494  
   495  func (yw *ycsbWorker) run(ctx context.Context) error {
   496  	op := yw.chooseOp()
   497  	var err error
   498  
   499  	start := timeutil.Now()
   500  	switch op {
   501  	case updateOp:
   502  		err = yw.updateRow(ctx)
   503  	case readOp:
   504  		err = yw.readRow(ctx)
   505  	case insertOp:
   506  		err = yw.insertRow(ctx)
   507  	case scanOp:
   508  		err = yw.scanRows(ctx)
   509  	case readModifyWriteOp:
   510  		err = yw.readModifyWriteRow(ctx)
   511  	default:
   512  		return errors.Errorf(`unknown operation: %s`, op)
   513  	}
   514  	if err != nil {
   515  		return err
   516  	}
   517  
   518  	elapsed := timeutil.Since(start)
   519  	yw.hists.Get(string(op)).Record(elapsed)
   520  	return nil
   521  }
   522  
   523  var readOnly int32
   524  
   525  type operation string
   526  
   527  const (
   528  	updateOp          operation = `update`
   529  	insertOp          operation = `insert`
   530  	readOp            operation = `read`
   531  	scanOp            operation = `scan`
   532  	readModifyWriteOp operation = `readModifyWrite`
   533  )
   534  
   535  func (yw *ycsbWorker) hashKey(key uint64) uint64 {
   536  	yw.hashBuf = [8]byte{} // clear hashBuf
   537  	binary.PutUvarint(yw.hashBuf[:], key)
   538  	yw.hashFunc.Reset()
   539  	if _, err := yw.hashFunc.Write(yw.hashBuf[:]); err != nil {
   540  		panic(err)
   541  	}
   542  	return yw.hashFunc.Sum64()
   543  }
   544  
   545  func (yw *ycsbWorker) buildKeyName(keynum uint64) string {
   546  	return keyNameFromHash(yw.hashKey(keynum))
   547  }
   548  
   549  func keyNameFromHash(hashedKey uint64) string {
   550  	return fmt.Sprintf("user%d", hashedKey)
   551  }
   552  
   553  // Keys are chosen by first drawing from a Zipf distribution, hashing the drawn
   554  // value, and modding by the total number of rows, so that not all hot keys are
   555  // close together.
   556  // See YCSB paper section 5.3 for a complete description of how keys are chosen.
   557  func (yw *ycsbWorker) nextReadKey() string {
   558  	rowCount := yw.rowCounter.Last()
   559  	// TODO(jeffreyxiao): The official YCSB implementation creates a very large
   560  	// key space for the zipfian distribution, hashes, mods it by the number of
   561  	// expected number of keys at the end of the workload to obtain the key index
   562  	// to read. The key index is then hashed again to obtain the key. If the
   563  	// generated key index is greater than the number of acknowledged rows, then
   564  	// the key is regenerated. Unfortunately, we cannot match this implementation
   565  	// since we run YCSB for a set amount of time rather than number of
   566  	// operations, so we don't know the expected number of keys at the end of the
   567  	// workload. Instead we directly use the zipfian generator to generate our
   568  	// key indexes by modding it by the actual number of rows. We don't hash so
   569  	// the hotspot is consistent and randomly distributed in the key space like
   570  	// with the official implementation. However, unlike the official
   571  	// implementation, this causes the hotspot to not be random w.r.t the
   572  	// insertion order of the keys (the hottest key is always the first inserted
   573  	// key). The distribution is also slightly different than the official YCSB's
   574  	// distribution, so it might be worthwhile to exactly emulate what they're
   575  	// doing.
   576  	rowIndex := yw.requestGen.Uint64() % rowCount
   577  	return yw.buildKeyName(rowIndex)
   578  }
   579  
   580  func (yw *ycsbWorker) nextInsertKeyIndex() uint64 {
   581  	if yw.nextInsertIndex != nil {
   582  		result := *yw.nextInsertIndex
   583  		yw.nextInsertIndex = nil
   584  		return result
   585  	}
   586  	return atomic.AddUint64(yw.rowIndex, 1) - 1
   587  }
   588  
   589  var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
   590  
   591  // Gnerate a random string of alphabetic characters.
   592  func (yw *ycsbWorker) randString(length int) string {
   593  	str := make([]byte, length)
   594  	for i := range str {
   595  		str[i] = letters[yw.rng.Intn(len(letters))]
   596  	}
   597  	return string(str)
   598  }
   599  
   600  func (yw *ycsbWorker) insertRow(ctx context.Context) error {
   601  	var args [numTableFields + 1]interface{}
   602  	keyIndex := yw.nextInsertKeyIndex()
   603  	args[0] = yw.buildKeyName(keyIndex)
   604  	for i := 1; i <= numTableFields; i++ {
   605  		args[i] = yw.randString(fieldLength)
   606  	}
   607  	if _, err := yw.insertStmt.ExecContext(ctx, args[:]...); err != nil {
   608  		yw.nextInsertIndex = new(uint64)
   609  		*yw.nextInsertIndex = keyIndex
   610  		return err
   611  	}
   612  
   613  	count, err := yw.rowCounter.Acknowledge(keyIndex)
   614  	if err != nil {
   615  		return err
   616  	}
   617  	return yw.requestGen.IncrementIMax(count)
   618  }
   619  
   620  func (yw *ycsbWorker) updateRow(ctx context.Context) error {
   621  	var stmt *gosql.Stmt
   622  	var args [2]interface{}
   623  	args[0] = yw.nextReadKey()
   624  	fieldIdx := yw.rng.Intn(numTableFields)
   625  	value := yw.randString(fieldLength)
   626  	if yw.config.json {
   627  		stmt = yw.updateStmts[0]
   628  		args[1] = fmt.Sprintf(`{"field%d": "%s"}`, fieldIdx, value)
   629  	} else {
   630  		stmt = yw.updateStmts[fieldIdx]
   631  		args[1] = value
   632  	}
   633  	if _, err := stmt.ExecContext(ctx, args[:]...); err != nil {
   634  		return err
   635  	}
   636  	return nil
   637  }
   638  
   639  func (yw *ycsbWorker) readRow(ctx context.Context) error {
   640  	key := yw.nextReadKey()
   641  	res, err := yw.readStmt.QueryContext(ctx, key)
   642  	if err != nil {
   643  		return err
   644  	}
   645  	defer res.Close()
   646  	for res.Next() {
   647  	}
   648  	return res.Err()
   649  }
   650  
   651  func (yw *ycsbWorker) scanRows(ctx context.Context) error {
   652  	key := yw.nextReadKey()
   653  	scanLength := yw.scanLengthGen.Uint64()
   654  	// We run the SELECT statement in a retry loop to handle retryable errors. The
   655  	// scan is large enough that it occasionally begins streaming results back to
   656  	// the client, and so if it then hits a ReadWithinUncertaintyIntervalError
   657  	// then it will return this error even if it is being run as an implicit
   658  	// transaction.
   659  	return crdb.Execute(func() error {
   660  		res, err := yw.scanStmt.QueryContext(ctx, key, scanLength)
   661  		if err != nil {
   662  			return err
   663  		}
   664  		defer res.Close()
   665  		for res.Next() {
   666  		}
   667  		return res.Err()
   668  	})
   669  }
   670  
   671  func (yw *ycsbWorker) readModifyWriteRow(ctx context.Context) error {
   672  	key := yw.nextReadKey()
   673  	newValue := yw.randString(fieldLength)
   674  	fieldIdx := yw.rng.Intn(numTableFields)
   675  	var args [2]interface{}
   676  	args[0] = key
   677  	err := crdb.ExecuteTx(ctx, yw.db, nil, func(tx *gosql.Tx) error {
   678  		var oldValue []byte
   679  		readStmt := yw.readFieldForUpdateStmts[fieldIdx]
   680  		if err := tx.StmtContext(ctx, readStmt).QueryRowContext(ctx, key).Scan(&oldValue); err != nil {
   681  			return err
   682  		}
   683  		var updateStmt *gosql.Stmt
   684  		if yw.config.json {
   685  			updateStmt = yw.updateStmts[0]
   686  			args[1] = fmt.Sprintf(`{"field%d": "%s"}`, fieldIdx, newValue)
   687  		} else {
   688  			updateStmt = yw.updateStmts[fieldIdx]
   689  			args[1] = newValue
   690  		}
   691  		_, err := tx.StmtContext(ctx, updateStmt).ExecContext(ctx, args[:]...)
   692  		return err
   693  	})
   694  	if errors.Is(err, gosql.ErrNoRows) && ctx.Err() != nil {
   695  		// Sometimes a context cancellation during a transaction can result in
   696  		// sql.ErrNoRows instead of the appropriate context.DeadlineExceeded. In
   697  		// this case, we just return ctx.Err(). See
   698  		// https://github.com/lib/pq/issues/874.
   699  		return ctx.Err()
   700  	}
   701  	return err
   702  }
   703  
   704  // Choose an operation in proportion to the frequencies.
   705  func (yw *ycsbWorker) chooseOp() operation {
   706  	p := yw.rng.Float32()
   707  	if atomic.LoadInt32(&readOnly) == 0 && p <= yw.config.updateFreq {
   708  		return updateOp
   709  	}
   710  	p -= yw.config.updateFreq
   711  	if atomic.LoadInt32(&readOnly) == 0 && p <= yw.config.insertFreq {
   712  		return insertOp
   713  	}
   714  	p -= yw.config.insertFreq
   715  	if atomic.LoadInt32(&readOnly) == 0 && p <= yw.config.readModifyWriteFreq {
   716  		return readModifyWriteOp
   717  	}
   718  	p -= yw.config.readModifyWriteFreq
   719  	// If both scanFreq and readFreq are 0 default to readOp if we've reached
   720  	// this point because readOnly is true.
   721  	if p <= yw.config.scanFreq {
   722  		return scanOp
   723  	}
   724  	return readOp
   725  }