github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/workloadccl/roachmartccl/roachmart.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  package roachmartccl
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	gosql "database/sql"
    15  	"fmt"
    16  	"math/rand"
    17  	"strings"
    18  
    19  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    20  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    21  	"github.com/cockroachdb/cockroach/pkg/workload"
    22  	"github.com/cockroachdb/cockroach/pkg/workload/histogram"
    23  	"github.com/cockroachdb/errors"
    24  	"github.com/spf13/pflag"
    25  )
    26  
    27  // These need to be kept in sync with the zones used when --geo is passed
    28  // to roachprod.
    29  //
    30  // TODO(benesch): avoid hardcoding these.
    31  var zones = []string{"us-central1-b", "us-west1-b", "europe-west2-b"}
    32  
    33  var usersSchema = func() string {
    34  	var buf bytes.Buffer
    35  	buf.WriteString(`(
    36  	zone STRING,
    37  	email STRING,
    38  	address STRING,
    39  	PRIMARY KEY (zone, email)
    40  ) PARTITION BY LIST (zone) (`)
    41  	for i, z := range zones {
    42  		if i > 0 {
    43  			buf.WriteByte(',')
    44  		}
    45  		buf.WriteString(fmt.Sprintf("\n\tPARTITION %[1]q VALUES IN ('%[1]s')", z))
    46  	}
    47  	buf.WriteString("\n)")
    48  	return buf.String()
    49  }()
    50  
    51  const (
    52  	ordersSchema = `(
    53  		user_zone STRING,
    54  		user_email STRING,
    55  		id INT,
    56  		fulfilled BOOL,
    57  		PRIMARY KEY (user_zone, user_email, id),
    58  		FOREIGN KEY (user_zone, user_email) REFERENCES users
    59  	) INTERLEAVE IN PARENT users (user_zone, user_email)`
    60  
    61  	defaultUsers  = 10000
    62  	defaultOrders = 100000
    63  
    64  	zoneLocationsStmt = `UPSERT INTO system.locations VALUES
    65  		('zone', 'us-east1-b', 33.0641249, -80.0433347),
    66  		('zone', 'us-west1-b', 45.6319052, -121.2010282),
    67  		('zone', 'europe-west2-b', 51.509865, 0)
    68  	`
    69  )
    70  
    71  type roachmart struct {
    72  	flags     workload.Flags
    73  	connFlags *workload.ConnFlags
    74  
    75  	seed          int64
    76  	partition     bool
    77  	localZone     string
    78  	localPercent  int
    79  	users, orders int
    80  }
    81  
    82  func init() {
    83  	workload.Register(roachmartMeta)
    84  }
    85  
    86  var roachmartMeta = workload.Meta{
    87  	Name:        `roachmart`,
    88  	Description: `Roachmart models a geo-distributed online storefront with users and orders`,
    89  	Version:     `1.0.0`,
    90  	New: func() workload.Generator {
    91  		g := &roachmart{}
    92  		g.flags.FlagSet = pflag.NewFlagSet(`roachmart`, pflag.ContinueOnError)
    93  		g.flags.Meta = map[string]workload.FlagMeta{
    94  			`local-zone`:    {RuntimeOnly: true},
    95  			`local-percent`: {RuntimeOnly: true},
    96  		}
    97  		g.flags.Int64Var(&g.seed, `seed`, 1, `Key hash seed.`)
    98  		g.flags.BoolVar(&g.partition, `partition`, true, `Whether to apply zone configs to the partitions of the users table.`)
    99  		g.flags.StringVar(&g.localZone, `local-zone`, ``, `The zone in which this load generator is running.`)
   100  		g.flags.IntVar(&g.localPercent, `local-percent`, 50, `Percent (0-100) of operations that operate on local data.`)
   101  		g.flags.IntVar(&g.users, `users`, defaultUsers, `Initial number of accounts in users table.`)
   102  		g.flags.IntVar(&g.orders, `orders`, defaultOrders, `Initial number of orders in orders table.`)
   103  		g.connFlags = workload.NewConnFlags(&g.flags)
   104  		return g
   105  	},
   106  }
   107  
   108  // Meta implements the Generator interface.
   109  func (m *roachmart) Meta() workload.Meta { return roachmartMeta }
   110  
   111  // Flags implements the Flagser interface.
   112  func (m *roachmart) Flags() workload.Flags { return m.flags }
   113  
   114  // Hooks implements the Hookser interface.
   115  func (m *roachmart) Hooks() workload.Hooks {
   116  	return workload.Hooks{
   117  		Validate: func() error {
   118  			if m.localZone == "" {
   119  				return errors.New("local zone must be specified")
   120  			}
   121  			found := false
   122  			for _, z := range zones {
   123  				if z == m.localZone {
   124  					found = true
   125  					break
   126  				}
   127  			}
   128  			if !found {
   129  				return fmt.Errorf("unknown zone %q (options: %s)", m.localZone, zones)
   130  			}
   131  			return nil
   132  		},
   133  
   134  		PreLoad: func(db *gosql.DB) error {
   135  			if _, err := db.Exec(zoneLocationsStmt); err != nil {
   136  				return err
   137  			}
   138  			if !m.partition {
   139  				return nil
   140  			}
   141  			for _, z := range zones {
   142  				// We are removing the EXPERIMENTAL keyword in 2.1. For compatibility
   143  				// with 2.0 clusters we still need to try with it if the
   144  				// syntax without EXPERIMENTAL fails.
   145  				// TODO(knz): Remove this in 2.2.
   146  				makeStmt := func(s string) string {
   147  					return fmt.Sprintf(s, fmt.Sprintf("%q", z), fmt.Sprintf("'constraints: [+zone=%s]'", z))
   148  				}
   149  				stmt := makeStmt("ALTER PARTITION %[1]s OF TABLE users CONFIGURE ZONE = %[2]s")
   150  				_, err := db.Exec(stmt)
   151  				if err != nil && strings.Contains(err.Error(), "syntax error") {
   152  					stmt = makeStmt("ALTER PARTITION %[1]s OF TABLE users EXPERIMENTAL CONFIGURE ZONE %[2]s")
   153  					_, err = db.Exec(stmt)
   154  				}
   155  				if err != nil {
   156  					return err
   157  				}
   158  			}
   159  			return nil
   160  		},
   161  	}
   162  }
   163  
   164  // Tables implements the Generator interface.
   165  func (m *roachmart) Tables() []workload.Table {
   166  	users := workload.Table{
   167  		Name:   `users`,
   168  		Schema: usersSchema,
   169  		InitialRows: workload.Tuples(
   170  			m.users,
   171  			func(rowIdx int) []interface{} {
   172  				rng := rand.New(rand.NewSource(m.seed + int64(rowIdx)))
   173  				const emailTemplate = `user-%d@roachmart.example`
   174  				return []interface{}{
   175  					zones[rowIdx%3],                     // zone
   176  					fmt.Sprintf(emailTemplate, rowIdx),  // email
   177  					string(randutil.RandBytes(rng, 64)), // address
   178  				}
   179  			},
   180  		),
   181  	}
   182  	orders := workload.Table{
   183  		Name:   `orders`,
   184  		Schema: ordersSchema,
   185  		InitialRows: workload.Tuples(
   186  			m.orders,
   187  			func(rowIdx int) []interface{} {
   188  				user := users.InitialRows.BatchRows(rowIdx % m.users)[0]
   189  				zone, email := user[0], user[1]
   190  				return []interface{}{
   191  					zone,                         // user_zone
   192  					email,                        // user_email
   193  					rowIdx,                       // id
   194  					[]string{`f`, `t`}[rowIdx%2], // fulfilled
   195  				}
   196  			},
   197  		),
   198  	}
   199  	return []workload.Table{users, orders}
   200  }
   201  
   202  // Ops implements the Opser interface.
   203  func (m *roachmart) Ops(urls []string, reg *histogram.Registry) (workload.QueryLoad, error) {
   204  	sqlDatabase, err := workload.SanitizeUrls(m, m.connFlags.DBOverride, urls)
   205  	if err != nil {
   206  		return workload.QueryLoad{}, err
   207  	}
   208  	db, err := gosql.Open(`cockroach`, strings.Join(urls, ` `))
   209  	if err != nil {
   210  		return workload.QueryLoad{}, err
   211  	}
   212  	// Allow a maximum of concurrency+1 connections to the database.
   213  	db.SetMaxOpenConns(m.connFlags.Concurrency + 1)
   214  	db.SetMaxIdleConns(m.connFlags.Concurrency + 1)
   215  
   216  	ql := workload.QueryLoad{SQLDatabase: sqlDatabase}
   217  
   218  	const query = `SELECT * FROM orders WHERE user_zone = $1 AND user_email = $2`
   219  	for i := 0; i < m.connFlags.Concurrency; i++ {
   220  		rng := rand.New(rand.NewSource(m.seed))
   221  		usersTable := m.Tables()[0]
   222  		hists := reg.GetHandle()
   223  		workerFn := func(ctx context.Context) error {
   224  			wantLocal := rng.Intn(100) < m.localPercent
   225  
   226  			// Pick a random user and advance until we have one that matches
   227  			// our locality requirements.
   228  			var zone, email interface{}
   229  			for i := rng.Int(); ; i++ {
   230  				user := usersTable.InitialRows.BatchRows(i % m.users)[0]
   231  				zone, email = user[0], user[1]
   232  				userLocal := zone == m.localZone
   233  				if userLocal == wantLocal {
   234  					break
   235  				}
   236  			}
   237  			start := timeutil.Now()
   238  			_, err := db.Exec(query, zone, email)
   239  			if wantLocal {
   240  				hists.Get(`local`).Record(timeutil.Since(start))
   241  			} else {
   242  				hists.Get(`remote`).Record(timeutil.Since(start))
   243  			}
   244  			return err
   245  		}
   246  		ql.WorkerFns = append(ql.WorkerFns, workerFn)
   247  	}
   248  	return ql, nil
   249  }