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 }