github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cmd/roachtest/bank.go (about) 1 // Copyright 2018 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 main 12 13 import ( 14 "bytes" 15 "context" 16 gosql "database/sql" 17 "fmt" 18 "math/rand" 19 "strconv" 20 "strings" 21 "sync" 22 "sync/atomic" 23 "time" 24 25 "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" 26 "github.com/cockroachdb/cockroach/pkg/testutils" 27 "github.com/cockroachdb/cockroach/pkg/util/randutil" 28 "github.com/cockroachdb/cockroach/pkg/util/retry" 29 "github.com/cockroachdb/cockroach/pkg/util/syncutil" 30 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 31 "github.com/cockroachdb/errors" 32 ) 33 34 const ( 35 bankStartingAmount = 999 36 bankMaxTransfer = 999 37 bankNumAccounts = 999 38 ) 39 40 type bankClient struct { 41 syncutil.RWMutex 42 db *gosql.DB 43 count uint64 44 } 45 46 func (client *bankClient) transferMoney(ctx context.Context, numAccounts, maxTransfer int) error { 47 from := rand.Intn(numAccounts) 48 to := rand.Intn(numAccounts - 1) 49 if from == to { 50 to = numAccounts - 1 51 } 52 amount := rand.Intn(maxTransfer) 53 54 tBegin := timeutil.Now() 55 56 // If this statement gets stuck, the test harness will get stuck. Run with a 57 // statement timeout, which unfortunately precludes the use of prepared 58 // statements. 59 q := fmt.Sprintf(` 60 SET statement_timeout = '30s'; 61 UPDATE bank.accounts 62 SET balance = CASE id WHEN %[1]d THEN balance-%[3]d WHEN %[2]d THEN balance+%[3]d END 63 WHERE id IN (%[1]d, %[2]d) AND (SELECT balance >= %[3]d FROM bank.accounts WHERE id = %[1]d); 64 `, from, to, amount) 65 66 client.RLock() 67 defer client.RUnlock() 68 _, err := client.db.ExecContext(ctx, q) 69 if err == nil { 70 // Do all increments under the read lock so that grabbing a write lock in 71 // startChaosMonkey below guarantees no more increments could be incoming. 72 atomic.AddUint64(&client.count, 1) 73 } 74 return errors.Wrapf(err, "after %.1fs", timeutil.Since(tBegin).Seconds()) 75 } 76 77 type bankState struct { 78 // One error sent by each client. A successful client sends a nil error. 79 errChan chan error 80 waitGroup sync.WaitGroup 81 // The number of times chaos monkey has run. 82 monkeyIteration uint64 83 // Set to 1 if chaos monkey has stalled the writes. 84 stalled int32 85 deadline time.Time 86 clients []bankClient 87 } 88 89 func (s *bankState) done(ctx context.Context) bool { 90 select { 91 case <-ctx.Done(): 92 return true 93 default: 94 } 95 return !timeutil.Now().Before(s.deadline) || atomic.LoadInt32(&s.stalled) == 1 96 } 97 98 // initClient initializes the client talking to node "i". 99 // It requires that the caller hold the client's write lock. 100 func (s *bankState) initClient(ctx context.Context, c *cluster, i int) { 101 s.clients[i-1].db = c.Conn(ctx, i) 102 } 103 104 // Returns counts from all the clients. 105 func (s *bankState) counts() []uint64 { 106 counts := make([]uint64, len(s.clients)) 107 for i := range s.clients { 108 counts[i] = atomic.LoadUint64(&s.clients[i].count) 109 } 110 return counts 111 } 112 113 // Initialize the "accounts" table. 114 func (s *bankState) initBank(ctx context.Context, t *test, c *cluster) { 115 db := c.Conn(ctx, 1) 116 defer db.Close() 117 118 if _, err := db.ExecContext(ctx, `CREATE DATABASE IF NOT EXISTS bank`); err != nil { 119 t.Fatal(err) 120 } 121 122 // Delete table created by a prior instance of a test. 123 if _, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS bank.accounts`); err != nil { 124 t.Fatal(err) 125 } 126 127 schema := ` 128 CREATE TABLE bank.accounts ( 129 id INT PRIMARY KEY, 130 balance INT NOT NULL 131 )` 132 if _, err := db.ExecContext(ctx, schema); err != nil { 133 t.Fatal(err) 134 } 135 136 var placeholders bytes.Buffer 137 var values []interface{} 138 for i := 0; i < bankNumAccounts; i++ { 139 if i > 0 { 140 placeholders.WriteString(", ") 141 } 142 fmt.Fprintf(&placeholders, "($%d, %d)", i+1, bankStartingAmount) 143 values = append(values, i) 144 } 145 stmt := `INSERT INTO bank.accounts (id, balance) VALUES ` + placeholders.String() 146 if _, err := db.ExecContext(ctx, stmt, values...); err != nil { 147 t.Fatal(err) 148 } 149 } 150 151 // Continuously transfers money until done(). 152 func (s *bankState) transferMoney( 153 ctx context.Context, l *logger, c *cluster, idx, numAccounts, maxTransfer int, 154 ) { 155 defer c.l.Printf("client %d shutting down\n", idx) 156 client := &s.clients[idx-1] 157 for !s.done(ctx) { 158 if err := client.transferMoney(ctx, numAccounts, maxTransfer); err != nil { 159 // Ignore some errors. 160 if !pgerror.IsSQLRetryableError(err) { 161 // Report the err and terminate. 162 s.errChan <- err 163 return 164 } 165 } 166 } 167 s.errChan <- nil 168 } 169 170 // Verify accounts. 171 func (s *bankState) verifyAccounts(ctx context.Context, t *test) { 172 select { 173 case <-ctx.Done(): 174 return 175 default: 176 } 177 178 client := &s.clients[0] 179 180 var sum int 181 var numAccounts uint64 182 err := retry.ForDuration(30*time.Second, func() error { 183 // Hold the read lock on the client to prevent it being restarted by 184 // chaos monkey. 185 client.RLock() 186 defer client.RUnlock() 187 err := client.db.QueryRowContext(ctx, "SELECT count(*), sum(balance) FROM bank.accounts").Scan(&numAccounts, &sum) 188 if err != nil && !pgerror.IsSQLRetryableError(err) { 189 t.Fatal(err) 190 } 191 return err 192 }) 193 if err != nil { 194 t.Fatal(err) 195 } 196 if expected := bankStartingAmount * bankNumAccounts; sum != expected { 197 t.Fatalf("the bank is not in good order, total value: %d, expected: %d", sum, expected) 198 } 199 200 if numAccounts != bankNumAccounts { 201 t.Fatalf("the bank is not in good order, total num accounts: %d, expected: %d", numAccounts, bankNumAccounts) 202 } 203 } 204 205 // startChaosMonkey picks a set of nodes and restarts them. 206 func (s *bankState) startChaosMonkey( 207 ctx context.Context, t *test, c *cluster, pickNodes func() []int, consistentIdx int, 208 ) { 209 s.waitGroup.Add(1) 210 go func() { 211 defer s.waitGroup.Done() 212 213 // Don't begin the chaos monkey until all nodes are serving SQL connections. 214 // This ensures that we don't test cluster initialization under chaos. 215 for i := 1; i <= c.spec.NodeCount; i++ { 216 db := c.Conn(ctx, i) 217 var res int 218 err := db.QueryRowContext(ctx, `SELECT 1`).Scan(&res) 219 if err != nil { 220 t.Fatal(err) 221 } 222 err = db.Close() 223 if err != nil { 224 t.Fatal(err) 225 } 226 } 227 228 for curRound := uint64(1); !s.done(ctx); curRound++ { 229 atomic.StoreUint64(&s.monkeyIteration, curRound) 230 231 // Pick nodes to be restarted. 232 nodes := pickNodes() 233 234 t.l.Printf("round %d: restarting nodes %v\n", curRound, nodes) 235 for _, i := range nodes { 236 if s.done(ctx) { 237 break 238 } 239 t.l.Printf("round %d: restarting %d\n", curRound, i) 240 c.Restart(ctx, t, c.Node(i)) 241 } 242 243 preCount := s.counts() 244 245 madeProgress := func() bool { 246 newCounts := s.counts() 247 for i := range newCounts { 248 if newCounts[i] > preCount[i] { 249 t.l.Printf("round %d: progress made by client %d\n", curRound, i) 250 return true 251 } 252 } 253 return false 254 } 255 256 // Sleep until at least one client is writing successfully. 257 c.l.Printf("round %d: monkey sleeping while cluster recovers...\n", curRound) 258 for !s.done(ctx) && !madeProgress() { 259 time.Sleep(time.Second) 260 } 261 if s.done(ctx) { 262 c.l.Printf("round %d: not waiting for recovery due to signal that we're done\n", 263 curRound) 264 return 265 } 266 267 t.l.Printf("round %d: cluster recovered\n", curRound) 268 } 269 }() 270 } 271 272 func (s *bankState) startSplitMonkey(ctx context.Context, d time.Duration, c *cluster) { 273 s.waitGroup.Add(1) 274 go func() { 275 defer s.waitGroup.Done() 276 277 r := newRand() 278 nodes := make([]string, c.spec.NodeCount) 279 280 for i := 0; i < c.spec.NodeCount; i++ { 281 nodes[i] = strconv.Itoa(i + 1) 282 } 283 284 for curRound := uint64(1); !s.done(ctx); curRound++ { 285 atomic.StoreUint64(&s.monkeyIteration, curRound) 286 time.Sleep(time.Duration(rand.Float64() * float64(d))) 287 288 client := &s.clients[c.All().randNode()[0]-1] 289 290 switch r.Intn(2) { 291 case 0: 292 client.RLock() 293 zipF := accountDistribution(r) 294 key := zipF.Uint64() 295 c.l.Printf("round %d: splitting key %v\n", curRound, key) 296 _, err := client.db.ExecContext(ctx, 297 fmt.Sprintf(`ALTER TABLE bank.accounts SPLIT AT VALUES (%d)`, key)) 298 if err != nil && !(pgerror.IsSQLRetryableError(err) || isExpectedRelocateError(err)) { 299 s.errChan <- err 300 } 301 client.RUnlock() 302 case 1: 303 for i := 0; i < len(s.clients); i++ { 304 s.clients[i].Lock() 305 } 306 zipF := accountDistribution(r) 307 key := zipF.Uint64() 308 309 rand.Shuffle(len(nodes), func(i, j int) { 310 nodes[i], nodes[j] = nodes[j], nodes[i] 311 }) 312 313 const relocateQueryFormat = `ALTER TABLE bank.accounts EXPERIMENTAL_RELOCATE VALUES (ARRAY[%s], %d);` 314 relocateQuery := fmt.Sprintf(relocateQueryFormat, strings.Join(nodes[1:], ", "), key) 315 c.l.Printf("round %d: relocating key %d to nodes %s\n", 316 curRound, key, nodes[1:]) 317 318 _, err := client.db.ExecContext(ctx, relocateQuery) 319 if err != nil && !(pgerror.IsSQLRetryableError(err) || isExpectedRelocateError(err)) { 320 s.errChan <- err 321 } 322 for i := 0; i < len(s.clients); i++ { 323 s.clients[i].Unlock() 324 } 325 } 326 } 327 }() 328 } 329 330 func isExpectedRelocateError(err error) bool { 331 // See: 332 // https://github.com/cockroachdb/cockroach/issues/33732 333 // https://github.com/cockroachdb/cockroach/issues/33708 334 // https://github.cm/cockroachdb/cockroach/issues/34012 335 // https://github.com/cockroachdb/cockroach/issues/33683#issuecomment-454889149 336 // for more failure modes not caught here. We decided to avoid adding 337 // to this catchall and to fix the root causes instead. 338 // We've also seen "breaker open" errors here. 339 whitelist := []string{ 340 "descriptor changed", 341 "unable to remove replica .* which is not present", 342 "unable to add replica .* which is already present", 343 "received invalid ChangeReplicasTrigger .* to remove self", 344 "failed to apply snapshot: raft group deleted", 345 "snapshot failed:", 346 } 347 pattern := "(" + strings.Join(whitelist, "|") + ")" 348 return testutils.IsError(err, pattern) 349 } 350 351 func accountDistribution(r *rand.Rand) *rand.Zipf { 352 // We use a Zipf distribution for selecting accounts. 353 return rand.NewZipf(r, 1.1, float64(bankNumAccounts/10), uint64(bankNumAccounts-1)) 354 } 355 356 func newRand() *rand.Rand { 357 return rand.New(rand.NewSource(timeutil.Now().UnixNano())) 358 } 359 360 // Wait until all clients have stopped. 361 func (s *bankState) waitClientsStop( 362 ctx context.Context, t *test, c *cluster, stallDuration time.Duration, 363 ) { 364 prevRound := atomic.LoadUint64(&s.monkeyIteration) 365 stallTime := timeutil.Now().Add(stallDuration) 366 var prevOutput string 367 // Spin until all clients are shut. 368 for doneClients := 0; doneClients < len(s.clients); { 369 select { 370 case <-ctx.Done(): 371 t.Fatal(ctx.Err()) 372 373 case err := <-s.errChan: 374 if err != nil { 375 t.Fatal(err) 376 } 377 doneClients++ 378 379 case <-time.After(time.Second): 380 var newOutput string 381 if timeutil.Now().Before(s.deadline) { 382 curRound := atomic.LoadUint64(&s.monkeyIteration) 383 if curRound == prevRound { 384 if timeutil.Now().After(stallTime) { 385 atomic.StoreInt32(&s.stalled, 1) 386 t.Fatalf("stall detected at round %d, no forward progress for %s", 387 curRound, stallDuration) 388 } 389 } else { 390 prevRound = curRound 391 stallTime = timeutil.Now().Add(stallDuration) 392 } 393 // Periodically print out progress so that we know the test is 394 // still running and making progress. 395 counts := s.counts() 396 strCounts := make([]string, len(counts)) 397 for i := range counts { 398 strCounts[i] = strconv.FormatUint(counts[i], 10) 399 } 400 newOutput = fmt.Sprintf("round %d: client counts: (%s)", 401 curRound, strings.Join(strCounts, ", ")) 402 } else { 403 newOutput = fmt.Sprintf("test finished, waiting for shutdown of %d clients", 404 c.spec.NodeCount-doneClients) 405 } 406 // This just stops the logs from being a bit too spammy. 407 if newOutput != prevOutput { 408 t.l.Printf("%s\n", newOutput) 409 prevOutput = newOutput 410 } 411 } 412 } 413 } 414 415 func runBankClusterRecovery(ctx context.Context, t *test, c *cluster) { 416 c.Put(ctx, cockroach, "./cockroach") 417 c.Start(ctx, t) 418 419 // TODO(peter): Run for longer when !local. 420 start := timeutil.Now() 421 s := &bankState{ 422 errChan: make(chan error, c.spec.NodeCount), 423 deadline: start.Add(time.Minute), 424 clients: make([]bankClient, c.spec.NodeCount), 425 } 426 s.initBank(ctx, t, c) 427 defer s.waitGroup.Wait() 428 429 for i := 0; i < c.spec.NodeCount; i++ { 430 s.clients[i].Lock() 431 s.initClient(ctx, c, i+1) 432 s.clients[i].Unlock() 433 go s.transferMoney(ctx, t.l, c, i+1, bankNumAccounts, bankMaxTransfer) 434 } 435 436 // Chaos monkey. 437 rnd, seed := randutil.NewPseudoRand() 438 t.l.Printf("monkey starts (seed %d)\n", seed) 439 pickNodes := func() []int { 440 nodes := rnd.Perm(c.spec.NodeCount)[:rnd.Intn(c.spec.NodeCount)+1] 441 for i := range nodes { 442 nodes[i]++ 443 } 444 return nodes 445 } 446 s.startChaosMonkey(ctx, t, c, pickNodes, -1) 447 448 s.waitClientsStop(ctx, t, c, 45*time.Second) 449 450 // Verify accounts. 451 s.verifyAccounts(ctx, t) 452 453 elapsed := timeutil.Since(start).Seconds() 454 var count uint64 455 counts := s.counts() 456 for _, c := range counts { 457 count += c 458 } 459 t.l.Printf("%d transfers (%.1f/sec) in %.1fs\n", count, float64(count)/elapsed, elapsed) 460 } 461 462 func runBankNodeRestart(ctx context.Context, t *test, c *cluster) { 463 c.Put(ctx, cockroach, "./cockroach") 464 c.Start(ctx, t) 465 466 // TODO(peter): Run for longer when !local. 467 start := timeutil.Now() 468 s := &bankState{ 469 errChan: make(chan error, 1), 470 deadline: start.Add(time.Minute), 471 clients: make([]bankClient, 1), 472 } 473 s.initBank(ctx, t, c) 474 defer s.waitGroup.Wait() 475 476 clientIdx := c.spec.NodeCount 477 client := &s.clients[0] 478 client.db = c.Conn(ctx, clientIdx) 479 480 go s.transferMoney(ctx, t.l, c, 1, bankNumAccounts, bankMaxTransfer) 481 482 // Chaos monkey. 483 rnd, seed := randutil.NewPseudoRand() 484 t.l.Printf("monkey starts (seed %d)\n", seed) 485 pickNodes := func() []int { 486 return []int{1 + rnd.Intn(clientIdx)} 487 } 488 s.startChaosMonkey(ctx, t, c, pickNodes, clientIdx) 489 490 s.waitClientsStop(ctx, t, c, 45*time.Second) 491 492 // Verify accounts. 493 s.verifyAccounts(ctx, t) 494 495 elapsed := timeutil.Since(start).Seconds() 496 count := atomic.LoadUint64(&client.count) 497 t.l.Printf("%d transfers (%.1f/sec) in %.1fs\n", count, float64(count)/elapsed, elapsed) 498 } 499 500 func runBankNodeZeroSum(ctx context.Context, t *test, c *cluster) { 501 c.Put(ctx, cockroach, "./cockroach") 502 c.Start(ctx, t) 503 504 start := timeutil.Now() 505 s := &bankState{ 506 errChan: make(chan error, c.spec.NodeCount), 507 deadline: start.Add(time.Minute), 508 clients: make([]bankClient, c.spec.NodeCount), 509 } 510 s.initBank(ctx, t, c) 511 defer s.waitGroup.Wait() 512 513 for i := 0; i < c.spec.NodeCount; i++ { 514 s.clients[i].Lock() 515 s.initClient(ctx, c, i+1) 516 s.clients[i].Unlock() 517 go s.transferMoney(ctx, t.l, c, i+1, bankNumAccounts, bankMaxTransfer) 518 } 519 520 s.startSplitMonkey(ctx, 2*time.Second, c) 521 s.waitClientsStop(ctx, t, c, 45*time.Second) 522 523 s.verifyAccounts(ctx, t) 524 525 elapsed := timeutil.Since(start).Seconds() 526 var count uint64 527 counts := s.counts() 528 for _, c := range counts { 529 count += c 530 } 531 c.l.Printf("%d transfers (%.1f/sec) in %.1fs\n", count, float64(count)/elapsed, elapsed) 532 } 533 534 var _ = runBankZeroSumRestart 535 536 func runBankZeroSumRestart(ctx context.Context, t *test, c *cluster) { 537 c.Put(ctx, cockroach, "./cockroach") 538 c.Start(ctx, t) 539 540 start := timeutil.Now() 541 s := &bankState{ 542 errChan: make(chan error, c.spec.NodeCount), 543 deadline: start.Add(time.Minute), 544 clients: make([]bankClient, c.spec.NodeCount), 545 } 546 s.initBank(ctx, t, c) 547 defer s.waitGroup.Wait() 548 549 for i := 0; i < c.spec.NodeCount; i++ { 550 s.clients[i].Lock() 551 s.initClient(ctx, c, i+1) 552 s.clients[i].Unlock() 553 go s.transferMoney(ctx, t.l, c, i+1, bankNumAccounts, bankMaxTransfer) 554 } 555 556 rnd, seed := randutil.NewPseudoRand() 557 c.l.Printf("monkey starts (seed %d)\n", seed) 558 pickNodes := func() []int { 559 nodes := rnd.Perm(c.spec.NodeCount)[:rnd.Intn(c.spec.NodeCount)+1] 560 for i := range nodes { 561 nodes[i]++ 562 } 563 return nodes 564 } 565 566 // Starting up the goroutines that restart and do splits and lease moves. 567 s.startChaosMonkey(ctx, t, c, pickNodes, -1) 568 s.startSplitMonkey(ctx, 2*time.Second, c) 569 s.waitClientsStop(ctx, t, c, 45*time.Second) 570 571 // Verify accounts. 572 s.verifyAccounts(ctx, t) 573 574 elapsed := timeutil.Since(start).Seconds() 575 var count uint64 576 counts := s.counts() 577 for _, c := range counts { 578 count += c 579 } 580 c.l.Printf("%d transfers (%.1f/sec) in %.1fs\n", count, float64(count)/elapsed, elapsed) 581 }