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 }