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  }