github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/tpcc/new_order.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 tpcc 12 13 import ( 14 "context" 15 "fmt" 16 "sort" 17 "strings" 18 "sync/atomic" 19 "time" 20 21 "github.com/cockroachdb/cockroach-go/crdb" 22 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 23 "github.com/cockroachdb/cockroach/pkg/workload" 24 "github.com/cockroachdb/errors" 25 "github.com/lib/pq" 26 "golang.org/x/exp/rand" 27 ) 28 29 // From the TPCC spec, section 2.4: 30 // 31 // The New-Order business transaction consists of entering a complete order 32 // through a single database transaction. It represents a mid-weight, read-write 33 // transaction with a high frequency of execution and stringent response time 34 // requirements to satisfy on-line users. This transaction is the backbone of 35 // the workload. It is designed to place a variable load on the system to 36 // reflect on-line database activity as typically found in production 37 // environments. 38 39 type orderItem struct { 40 olSupplyWID int // supplying warehouse id 41 olIID int // item id 42 olNumber int // item number in order 43 iName string // item name 44 olQuantity int // order quantity 45 brandGeneric string 46 iPrice float64 // item price 47 olAmount float64 // order amount 48 olDeliveryD pq.NullTime 49 50 remoteWarehouse bool // internal use - item from a local or remote warehouse? 51 } 52 53 type newOrderData struct { 54 // This data must all be returned by the transaction. See 2.4.3.3. 55 wID int // home warehouse ID 56 dID int // district id 57 cID int // customer id 58 oID int // order id 59 oOlCnt int // order line count 60 cLast string 61 cCredit string 62 cDiscount float64 63 wTax float64 64 dTax float64 65 oEntryD time.Time 66 totalAmount float64 67 68 items []orderItem 69 } 70 71 var errSimulated = errors.New("simulated user error") 72 73 type newOrder struct { 74 config *tpcc 75 mcp *workload.MultiConnPool 76 sr workload.SQLRunner 77 78 updateDistrict workload.StmtHandle 79 selectWarehouseTax workload.StmtHandle 80 selectCustomerInfo workload.StmtHandle 81 insertOrder workload.StmtHandle 82 insertNewOrder workload.StmtHandle 83 } 84 85 var _ tpccTx = &newOrder{} 86 87 func createNewOrder( 88 ctx context.Context, config *tpcc, mcp *workload.MultiConnPool, 89 ) (tpccTx, error) { 90 n := &newOrder{ 91 config: config, 92 mcp: mcp, 93 } 94 95 // Select the district tax rate and next available order number, bumping it. 96 n.updateDistrict = n.sr.Define(` 97 UPDATE district 98 SET d_next_o_id = d_next_o_id + 1 99 WHERE d_w_id = $1 AND d_id = $2 100 RETURNING d_tax, d_next_o_id`, 101 ) 102 103 // Select the warehouse tax rate. 104 n.selectWarehouseTax = n.sr.Define(` 105 SELECT w_tax FROM warehouse WHERE w_id = $1`, 106 ) 107 108 // Select the customer's discount, last name and credit. 109 n.selectCustomerInfo = n.sr.Define(` 110 SELECT c_discount, c_last, c_credit 111 FROM customer 112 WHERE c_w_id = $1 AND c_d_id = $2 AND c_id = $3`, 113 ) 114 115 n.insertOrder = n.sr.Define(` 116 INSERT INTO "order" (o_id, o_d_id, o_w_id, o_c_id, o_entry_d, o_ol_cnt, o_all_local) 117 VALUES ($1, $2, $3, $4, $5, $6, $7)`, 118 ) 119 120 n.insertNewOrder = n.sr.Define(` 121 INSERT INTO new_order (no_o_id, no_d_id, no_w_id) 122 VALUES ($1, $2, $3)`, 123 ) 124 125 if err := n.sr.Init(ctx, "new-order", mcp, config.connFlags); err != nil { 126 return nil, err 127 } 128 129 return n, nil 130 } 131 132 func (n *newOrder) run(ctx context.Context, wID int) (interface{}, error) { 133 atomic.AddUint64(&n.config.auditor.newOrderTransactions, 1) 134 135 rng := rand.New(rand.NewSource(uint64(timeutil.Now().UnixNano()))) 136 137 d := newOrderData{ 138 wID: wID, 139 dID: int(randInt(rng, 1, 10)), 140 cID: n.config.randCustomerID(rng), 141 oOlCnt: int(randInt(rng, 5, 15)), 142 } 143 d.items = make([]orderItem, d.oOlCnt) 144 145 n.config.auditor.Lock() 146 n.config.auditor.orderLinesFreq[d.oOlCnt]++ 147 n.config.auditor.Unlock() 148 atomic.AddUint64(&n.config.auditor.totalOrderLines, uint64(d.oOlCnt)) 149 150 // itemIDs tracks the item ids in the order so that we can prevent adding 151 // multiple items with the same ID. This would not make sense because each 152 // orderItem already tracks a quantity that can be larger than 1. 153 itemIDs := make(map[int]struct{}) 154 155 // 2.4.1.4: A fixed 1% of the New-Order transactions are chosen at random to 156 // simulate user data entry errors and exercise the performance of rolling 157 // back update transactions. 158 rollback := rng.Intn(100) == 0 159 160 // allLocal tracks whether any of the items were from a remote warehouse. 161 allLocal := 1 162 for i := 0; i < d.oOlCnt; i++ { 163 item := orderItem{ 164 olNumber: i + 1, 165 // 2.4.1.5.3: order has a quantity [1..10] 166 olQuantity: rng.Intn(10) + 1, 167 } 168 // 2.4.1.5.1 an order item has a random item number, unless rollback is true 169 // and it's the last item in the items list. 170 if rollback && i == d.oOlCnt-1 { 171 item.olIID = -1 172 } else { 173 // Loop until we find a unique item ID. 174 for { 175 item.olIID = n.config.randItemID(rng) 176 if _, ok := itemIDs[item.olIID]; !ok { 177 itemIDs[item.olIID] = struct{}{} 178 break 179 } 180 } 181 } 182 // 2.4.1.5.2: 1% of the time, an item is supplied from a remote warehouse. 183 item.remoteWarehouse = rng.Intn(100) == 0 184 item.olSupplyWID = wID 185 if item.remoteWarehouse && n.config.activeWarehouses > 1 { 186 allLocal = 0 187 // To avoid picking the local warehouse again, randomly choose among n-1 188 // warehouses and swap in the nth if necessary. 189 item.olSupplyWID = n.config.wPart.randActive(rng) 190 for item.olSupplyWID == wID { 191 item.olSupplyWID = n.config.wPart.randActive(rng) 192 } 193 n.config.auditor.Lock() 194 n.config.auditor.orderLineRemoteWarehouseFreq[item.olSupplyWID]++ 195 n.config.auditor.Unlock() 196 } else { 197 item.olSupplyWID = wID 198 } 199 d.items[i] = item 200 } 201 202 // Sort the items in the same order that we will require from batch select queries. 203 sort.Slice(d.items, func(i, j int) bool { 204 return d.items[i].olIID < d.items[j].olIID 205 }) 206 207 d.oEntryD = timeutil.Now() 208 209 tx, err := n.mcp.Get().BeginEx(ctx, n.config.txOpts) 210 if err != nil { 211 return nil, err 212 } 213 err = crdb.ExecuteInTx( 214 ctx, (*workload.PgxTx)(tx), 215 func() error { 216 // Select the district tax rate and next available order number, bumping it. 217 var dNextOID int 218 if err := n.updateDistrict.QueryRowTx( 219 ctx, tx, d.wID, d.dID, 220 ).Scan(&d.dTax, &dNextOID); err != nil { 221 return err 222 } 223 d.oID = dNextOID - 1 224 225 // Select the warehouse tax rate. 226 if err := n.selectWarehouseTax.QueryRowTx( 227 ctx, tx, wID, 228 ).Scan(&d.wTax); err != nil { 229 return err 230 } 231 232 // Select the customer's discount, last name and credit. 233 if err := n.selectCustomerInfo.QueryRowTx( 234 ctx, tx, d.wID, d.dID, d.cID, 235 ).Scan(&d.cDiscount, &d.cLast, &d.cCredit); err != nil { 236 return err 237 } 238 239 // 2.4.2.2: For each o_ol_cnt item in the order, query the relevant item 240 // row, update the stock row to account for the order, and insert a new 241 // line into the order_line table to reflect the item on the order. 242 itemIDs := make([]string, d.oOlCnt) 243 for i, item := range d.items { 244 itemIDs[i] = fmt.Sprint(item.olIID) 245 } 246 rows, err := tx.QueryEx( 247 ctx, 248 fmt.Sprintf(` 249 SELECT i_price, i_name, i_data 250 FROM item 251 WHERE i_id IN (%[1]s) 252 ORDER BY i_id`, 253 strings.Join(itemIDs, ", "), 254 ), 255 nil, /* options */ 256 ) 257 if err != nil { 258 return err 259 } 260 iDatas := make([]string, d.oOlCnt) 261 for i := range d.items { 262 item := &d.items[i] 263 iData := &iDatas[i] 264 265 if !rows.Next() { 266 if err := rows.Err(); err != nil { 267 return err 268 } 269 if rollback { 270 // 2.4.2.3: roll back when we're expecting a rollback due to 271 // simulated user error (invalid item id) and we actually 272 // can't find the item. The spec requires us to actually go 273 // to the database for this, even though we know earlier 274 // that the item has an invalid number. 275 atomic.AddUint64(&n.config.auditor.newOrderRollbacks, 1) 276 return errSimulated 277 } 278 return errors.New("missing item row") 279 } 280 281 err = rows.Scan(&item.iPrice, &item.iName, iData) 282 if err != nil { 283 rows.Close() 284 return err 285 } 286 } 287 if rows.Next() { 288 return errors.New("extra item row") 289 } 290 if err := rows.Err(); err != nil { 291 return err 292 } 293 rows.Close() 294 295 stockIDs := make([]string, d.oOlCnt) 296 for i, item := range d.items { 297 stockIDs[i] = fmt.Sprintf("(%d, %d)", item.olIID, item.olSupplyWID) 298 } 299 rows, err = tx.QueryEx( 300 ctx, 301 fmt.Sprintf(` 302 SELECT s_quantity, s_ytd, s_order_cnt, s_remote_cnt, s_data, s_dist_%02[1]d 303 FROM stock 304 WHERE (s_i_id, s_w_id) IN (%[2]s) 305 ORDER BY s_i_id`, 306 d.dID, strings.Join(stockIDs, ", "), 307 ), 308 nil, /* options */ 309 ) 310 if err != nil { 311 return err 312 } 313 distInfos := make([]string, d.oOlCnt) 314 sQuantityUpdateCases := make([]string, d.oOlCnt) 315 sYtdUpdateCases := make([]string, d.oOlCnt) 316 sOrderCntUpdateCases := make([]string, d.oOlCnt) 317 sRemoteCntUpdateCases := make([]string, d.oOlCnt) 318 for i := range d.items { 319 item := &d.items[i] 320 321 if !rows.Next() { 322 if err := rows.Err(); err != nil { 323 return err 324 } 325 return errors.New("missing stock row") 326 } 327 328 var sQuantity, sYtd, sOrderCnt, sRemoteCnt int 329 var sData string 330 err = rows.Scan(&sQuantity, &sYtd, &sOrderCnt, &sRemoteCnt, &sData, &distInfos[i]) 331 if err != nil { 332 rows.Close() 333 return err 334 } 335 336 if strings.Contains(sData, originalString) && strings.Contains(iDatas[i], originalString) { 337 item.brandGeneric = "B" 338 } else { 339 item.brandGeneric = "G" 340 } 341 342 newSQuantity := sQuantity - item.olQuantity 343 if sQuantity < item.olQuantity+10 { 344 newSQuantity += 91 345 } 346 347 newSRemoteCnt := sRemoteCnt 348 if item.remoteWarehouse { 349 newSRemoteCnt++ 350 } 351 352 sQuantityUpdateCases[i] = fmt.Sprintf("WHEN %s THEN %d", stockIDs[i], newSQuantity) 353 sYtdUpdateCases[i] = fmt.Sprintf("WHEN %s THEN %d", stockIDs[i], sYtd+item.olQuantity) 354 sOrderCntUpdateCases[i] = fmt.Sprintf("WHEN %s THEN %d", stockIDs[i], sOrderCnt+1) 355 sRemoteCntUpdateCases[i] = fmt.Sprintf("WHEN %s THEN %d", stockIDs[i], newSRemoteCnt) 356 } 357 if rows.Next() { 358 return errors.New("extra stock row") 359 } 360 if err := rows.Err(); err != nil { 361 return err 362 } 363 rows.Close() 364 365 // Insert row into the orders and new orders table. 366 if _, err := n.insertOrder.ExecTx( 367 ctx, tx, 368 d.oID, d.dID, d.wID, d.cID, d.oEntryD.Format("2006-01-02 15:04:05"), d.oOlCnt, allLocal, 369 ); err != nil { 370 return err 371 } 372 if _, err := n.insertNewOrder.ExecTx( 373 ctx, tx, d.oID, d.dID, d.wID, 374 ); err != nil { 375 return err 376 } 377 378 // Update the stock table for each item. 379 if _, err := tx.ExecEx( 380 ctx, 381 fmt.Sprintf(` 382 UPDATE stock 383 SET 384 s_quantity = CASE (s_i_id, s_w_id) %[1]s ELSE crdb_internal.force_error('', 'unknown case') END, 385 s_ytd = CASE (s_i_id, s_w_id) %[2]s END, 386 s_order_cnt = CASE (s_i_id, s_w_id) %[3]s END, 387 s_remote_cnt = CASE (s_i_id, s_w_id) %[4]s END 388 WHERE (s_i_id, s_w_id) IN (%[5]s)`, 389 strings.Join(sQuantityUpdateCases, " "), 390 strings.Join(sYtdUpdateCases, " "), 391 strings.Join(sOrderCntUpdateCases, " "), 392 strings.Join(sRemoteCntUpdateCases, " "), 393 strings.Join(stockIDs, ", "), 394 ), 395 nil, /* options */ 396 ); err != nil { 397 return err 398 } 399 400 // Insert a new order line for each item in the order. 401 olValsStrings := make([]string, d.oOlCnt) 402 for i := range d.items { 403 item := &d.items[i] 404 item.olAmount = float64(item.olQuantity) * item.iPrice 405 d.totalAmount += item.olAmount 406 407 olValsStrings[i] = fmt.Sprintf("(%d,%d,%d,%d,%d,%d,%d,%f,'%s')", 408 d.oID, // ol_o_id 409 d.dID, // ol_d_id 410 d.wID, // ol_w_id 411 item.olNumber, // ol_number 412 item.olIID, // ol_i_id 413 item.olSupplyWID, // ol_supply_w_id 414 item.olQuantity, // ol_quantity 415 item.olAmount, // ol_amount 416 distInfos[i], // ol_dist_info 417 ) 418 } 419 if _, err := tx.ExecEx( 420 ctx, 421 fmt.Sprintf(` 422 INSERT INTO order_line(ol_o_id, ol_d_id, ol_w_id, ol_number, ol_i_id, ol_supply_w_id, ol_quantity, ol_amount, ol_dist_info) 423 VALUES %s`, 424 strings.Join(olValsStrings, ", "), 425 ), 426 nil, /* options */ 427 ); err != nil { 428 return err 429 } 430 431 // 2.4.2.2: total_amount = sum(OL_AMOUNT) * (1 - C_DISCOUNT) * (1 + W_TAX + D_TAX) 432 d.totalAmount *= (1 - d.cDiscount) * (1 + d.wTax + d.dTax) 433 434 return nil 435 }) 436 if errors.Is(err, errSimulated) { 437 return d, nil 438 } 439 return d, err 440 }