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  }