github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/workload/interleavedpartitioned/interleavedpartitioned.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 interleavedpartitioned
    12  
    13  import (
    14  	"context"
    15  	gosql "database/sql"
    16  	"fmt"
    17  	"math/rand"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/cockroachdb/cockroach-go/crdb"
    22  	"github.com/cockroachdb/cockroach/pkg/col/coldata"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    24  	"github.com/cockroachdb/cockroach/pkg/util/bufalloc"
    25  	"github.com/cockroachdb/cockroach/pkg/util/log"
    26  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    27  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    28  	"github.com/cockroachdb/cockroach/pkg/workload"
    29  	"github.com/cockroachdb/cockroach/pkg/workload/histogram"
    30  	"github.com/cockroachdb/errors"
    31  	"github.com/spf13/pflag"
    32  )
    33  
    34  const (
    35  	zoneLocationsStmt = `
    36  UPSERT INTO system.locations VALUES
    37  	('zone', $1, 33.0641249, -80.0433347),
    38  	('zone', $2, 45.6319052, -121.2010282),
    39  	('zone', $3, 41.238785 , -95.854239)
    40  `
    41  
    42  	nodeIDQuery = `
    43  SELECT DISTINCT node_id
    44  FROM crdb_internal.node_build_info
    45  `
    46  
    47  	// Table Schemas
    48  	// TODO(bram): Deletes are very slow due to contention. We could create a
    49  	// partitioned index on session, but to do so would require creating a
    50  	// computed column (for east/west) and creating the index on that column and
    51  	// created.
    52  	sessionSchema = `
    53  (
    54  	session_id STRING(100) PRIMARY KEY,
    55  	affiliate STRING(100) NOT NULL,
    56  	channel STRING(50) NOT NULL,
    57  	language STRING(20) NOT NULL,
    58  	created TIMESTAMP NOT NULL,
    59  	updated TIMESTAMP NOT NULL,
    60  	status STRING(20) NOT NULL,
    61  	platform STRING(50) NOT NULL,
    62  	query_id STRING(100) NOT NULL
    63  ) PARTITION BY RANGE (session_id) (
    64  	PARTITION east VALUES FROM ('E-') TO ('F-'),
    65  	PARTITION west VALUES FROM ('W-') TO ('X-'),
    66  	PARTITION central VALUES FROM ('C-') TO ('D-')
    67  )`
    68  	genericChildSchema = `
    69  (
    70  	session_id STRING(100) NOT NULL,
    71  	id STRING(50) NOT NULL,
    72  	value STRING(50) NOT NULL,
    73  	created TIMESTAMP NOT NULL,
    74  	updated TIMESTAMP NOT NULL,
    75  	PRIMARY KEY (session_id, id),
    76  	FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
    77  ) INTERLEAVE IN PARENT sessions(session_id)`
    78  	deviceSchema = `
    79  (
    80  	session_id STRING(100) NOT NULL,
    81  	id STRING(100) NOT NULL,
    82  	device_id STRING(50),
    83  	name STRING(50),
    84  	make STRING(50),
    85  	macaddress STRING(50),
    86  	model STRING(50),
    87  	serial_number STRING(50),
    88  	created TIMESTAMP NOT NULL,
    89  	updated TIMESTAMP NOT NULL,
    90  	PRIMARY KEY (session_id, id),
    91  	FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
    92  ) INTERLEAVE IN PARENT sessions(session_id)
    93  `
    94  	querySchema = `
    95  (
    96  	session_id STRING(100) NOT NULL,
    97  	id STRING(50) NOT NULL,
    98  	created TIMESTAMP NOT NULL,
    99  	updated TIMESTAMP NOT NULL,
   100  	PRIMARY KEY (session_id, id),
   101  	FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
   102  ) INTERLEAVE IN PARENT sessions(session_id)
   103  `
   104  
   105  	// Insert Queries
   106  	insertQuery = `INSERT INTO sessions(
   107  	session_id,
   108  	affiliate,
   109  	channel,
   110  	language,
   111  	created,
   112  	updated,
   113  	status,
   114  	platform,
   115  	query_id
   116  ) VALUES ($1, $2, $3, $4, now(), now(), $5, $6, $7)
   117  `
   118  	insertQueryCustomers = `
   119  INSERT INTO customers(session_id, id, value, created, updated)
   120  VALUES ($1, $2, $3, now(), now())
   121  `
   122  	insertQueryVariants = `
   123  INSERT INTO variants(session_id, id, value, created, updated)
   124  VALUES ($1, $2, $3, now(), now())
   125  `
   126  	insertQueryParameters = `
   127  INSERT INTO parameters(session_id, id, value, created, updated)
   128  VALUES ($1, $2, $3, now(), now())
   129  `
   130  	insertQueryDevices = `
   131  INSERT INTO devices(
   132  	session_id,
   133  	id,
   134  	device_id,
   135  	name,
   136  	make,
   137  	macaddress,
   138  	model,
   139  	serial_number,
   140  	created,
   141  	updated
   142  ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now(), now())
   143  `
   144  	insertQueryQuery = `
   145  INSERT INTO queries(session_id, id, created, updated)
   146  VALUES ($1, $2, now(), now())
   147  `
   148  
   149  	// Delete queries
   150  	deleteWestQuery = `
   151  DELETE FROM sessions
   152  WHERE session_id LIKE 'W-%'
   153  	AND created < now() - interval '1' minute
   154  LIMIT $1
   155  `
   156  	deleteEastQuery = `
   157  DELETE FROM sessions
   158  WHERE session_id LIKE 'E-%'
   159  	AND created < now() - interval '1' minute
   160  LIMIT $1
   161  `
   162  
   163  	// Retrieve queries
   164  	retrieveQuery1 = `
   165  SELECT session_id, affiliate, channel, created, language, status, platform, query_id, updated
   166  FROM sessions
   167  WHERE session_id = $1
   168  `
   169  	retrieveQuery2 = `
   170  SELECT
   171  	device.id,
   172  	device.session_id,
   173  	device.created,
   174  	device.name,
   175  	device.model,
   176  	device.macaddress,
   177  	device.serial_number,
   178  	device.make,
   179  	device.updated,
   180  	session.session_id,
   181  	session.affiliate,
   182  	session.created,
   183  	session.channel,
   184  	session.language,
   185  	session.status,
   186  	session.platform,
   187  	session.query_id,
   188  	session.updated
   189  	FROM sessions as session
   190  	LEFT OUTER JOIN devices AS device
   191  	ON session.session_id = device.session_id
   192  	WHERE session.session_id = $1
   193  `
   194  	retrieveQuery3 = `
   195  UPDATE sessions
   196  SET updated = now()
   197  WHERE session_id = $1
   198  `
   199  	retrieveQuery4 = `
   200  SELECT session_id, id, id, session_id, created, value, updated
   201  FROM customers
   202  WHERE session_id = $1
   203  `
   204  	retrieveQuery5 = `
   205  SELECT session_id, id, id, session_id, created, value, updated
   206  FROM parameters
   207  WHERE session_id = $1
   208  `
   209  	retrieveQuery6 = `
   210  SELECT session_id, id, id, session_id, created, value, updated
   211  FROM variants
   212  WHERE session_id = $1
   213  `
   214  	retrieveQuery7 = `
   215  SELECT d.session_id AS device_session_id,
   216  	d.created AS device_created,
   217  	d.device_id AS device_id,
   218  	d.make AS make,
   219  	d.model AS model,
   220  	d.name AS name,
   221  	d.serial_number AS name,
   222  	d.updated AS device_updated,
   223  	s.session_id AS session_id,
   224  	s.affiliate AS affiliate,
   225  	s.channel AS channel,
   226  	s.created AS session_created,
   227  	s.language AS language,
   228  	s.platform AS platform,
   229  	s.query_id AS query_id,
   230  	s.status AS status,
   231  	s.updated AS session_updated
   232  FROM devices AS d
   233  LEFT JOIN sessions AS s
   234  ON d.session_id = s.session_id
   235  WHERE d.session_id = $1
   236  `
   237  
   238  	// Update Queries
   239  	updateQuery1 = `
   240  UPDATE sessions
   241  SET query_id = $1, updated = now()
   242  WHERE session_id = $2
   243  `
   244  	updateQuery2 = `
   245  UPDATE sessions
   246  SET status = $1, updated = now()
   247  WHERE session_id = $2
   248  `
   249  
   250  	// Fetch random session ID queries.
   251  	findSessionIDQuery1 = `
   252  SELECT session_id
   253  FROM sessions
   254  WHERE session_id > $1
   255  LIMIT 1
   256  `
   257  	findSessionIDQuery2 = `
   258  SELECT session_id
   259  FROM sessions
   260  WHERE session_id > $1
   261  LIMIT 1
   262  `
   263  )
   264  
   265  var (
   266  	retrieveQueries = []string{
   267  		retrieveQuery1,
   268  		retrieveQuery2,
   269  		retrieveQuery3,
   270  		retrieveQuery4,
   271  		retrieveQuery5,
   272  		retrieveQuery6,
   273  		retrieveQuery7,
   274  	}
   275  
   276  	// All retrieve queries are run in an update operation before the update
   277  	// queries.
   278  	updateQueries = []string{
   279  		updateQuery1,
   280  		updateQuery2,
   281  	}
   282  )
   283  
   284  func init() {
   285  	workload.Register(interleavedPartitionedMeta)
   286  }
   287  
   288  type interleavedPartitioned struct {
   289  	flags     workload.Flags
   290  	connFlags *workload.ConnFlags
   291  
   292  	locality string
   293  
   294  	// data distribution flags
   295  	customersPerSession  int
   296  	devicesPerSession    int
   297  	variantsPerSession   int
   298  	parametersPerSession int
   299  	queriesPerSession    int
   300  
   301  	// flags for initial db loading
   302  	initEastPercent int
   303  	initSessions    int
   304  
   305  	// operation flags
   306  	insertPercent        int
   307  	insertLocalPercent   int
   308  	retrievePercent      int
   309  	retrieveLocalPercent int
   310  	updatePercent        int
   311  	updateLocalPercent   int
   312  
   313  	// info for delete jobs
   314  	deletes       bool // set based on zone, not a flag
   315  	rowsPerDelete int
   316  
   317  	// zones
   318  	eastZoneName    string
   319  	westZoneName    string
   320  	centralZoneName string
   321  
   322  	// prepared statements
   323  	retrieveStatements       []*gosql.Stmt
   324  	updateStatements         []*gosql.Stmt
   325  	insertStatement          *gosql.Stmt
   326  	insertCustomerStatement  *gosql.Stmt
   327  	insertDeviceStatement    *gosql.Stmt
   328  	insertVariantStatement   *gosql.Stmt
   329  	insertParameterStatement *gosql.Stmt
   330  	insertQueryStatement     *gosql.Stmt
   331  	deleteEastStatement      *gosql.Stmt
   332  	deleteWestStatement      *gosql.Stmt
   333  	findSessionIDStatement1  *gosql.Stmt
   334  	findSessionIDStatement2  *gosql.Stmt
   335  }
   336  
   337  var interleavedPartitionedMeta = workload.Meta{
   338  	Name:        `interleavedpartitioned`,
   339  	Description: `Tests the performance of tables that are both interleaved and partitioned`,
   340  	Version:     `1.0.0`,
   341  	New: func() workload.Generator {
   342  		g := &interleavedPartitioned{}
   343  		g.flags.FlagSet = pflag.NewFlagSet(`interleavedpartitioned`, pflag.ContinueOnError)
   344  		g.flags.Meta = map[string]workload.FlagMeta{
   345  			`batch`: {RuntimeOnly: true},
   346  		}
   347  		g.flags.IntVar(&g.initSessions, `init-sessions`, 1000, `Number of sessions (rows in the parent table) to create during initialization`)
   348  		g.flags.IntVar(&g.customersPerSession, `customers-per-session`, 2, `Number of customers associated with each session`)
   349  		g.flags.IntVar(&g.devicesPerSession, `devices-per-session`, 2, `Number of devices associated with each session`)
   350  		g.flags.IntVar(&g.variantsPerSession, `variants-per-session`, 5, `Number of variants associated with each session`)
   351  		g.flags.IntVar(&g.parametersPerSession, `parameters-per-session`, 1, `Number of parameters associated with each session`)
   352  		g.flags.IntVar(&g.queriesPerSession, `queries-per-session`, 1, `Number of queries associated with each session`)
   353  		g.flags.IntVar(&g.initEastPercent, `init-east-percent`, 50, `Percentage (0-100) of sessions that are in us-east used when initializing rows only`)
   354  		g.flags.IntVar(&g.insertPercent, `insert-percent`, 70, `Percentage (0-100) of operations that are inserts`)
   355  		g.flags.IntVar(&g.insertLocalPercent, `insert-local-percent`, 100, `Percentage of insert operations that are local`)
   356  		g.flags.IntVar(&g.retrievePercent, `retrieve-percent`, 20, `Percentage (0-100) of operations that are retrieval queries`)
   357  		g.flags.IntVar(&g.retrieveLocalPercent, `retrieve-local-percent`, 100, `Percentage of retrieve operations that are local`)
   358  		g.flags.IntVar(&g.updatePercent, `update-percent`, 10, `Percentage (0-100) of operations that are update queries`)
   359  		g.flags.IntVar(&g.updateLocalPercent, `update-local-percent`, 100, `Percentage of update operations that are local`)
   360  		g.flags.IntVar(&g.rowsPerDelete, `rows-per-delete`, 1, `Number of rows per delete operation`)
   361  		g.flags.StringVar(&g.eastZoneName, `east-zone-name`, `us-east1-b`, `Name of the zone to be used as east`)
   362  		g.flags.StringVar(&g.westZoneName, `west-zone-name`, `us-west1-b`, `Name of the zone to be used as west`)
   363  		g.flags.StringVar(&g.centralZoneName, `central-zone-name`, `us-central1-a`, `Name of the zone to be used as central`)
   364  		g.flags.StringVar(&g.locality, `locality`, ``, `Which locality is the workload running in? (east,west,central)`)
   365  		g.connFlags = workload.NewConnFlags(&g.flags)
   366  		return g
   367  	},
   368  }
   369  
   370  // Meta implements the Generator interface.
   371  func (w *interleavedPartitioned) Meta() workload.Meta { return interleavedPartitionedMeta }
   372  
   373  // Flags implements the Flagser interface.
   374  func (w *interleavedPartitioned) Flags() workload.Flags { return w.flags }
   375  
   376  // Tables implements the Generator interface.
   377  func (w *interleavedPartitioned) Tables() []workload.Table {
   378  	sessionsTable := workload.Table{
   379  		Name:   `sessions`,
   380  		Schema: sessionSchema,
   381  		InitialRows: workload.Tuples(
   382  			w.initSessions,
   383  			w.sessionsInitialRow,
   384  		),
   385  	}
   386  	customerTable := workload.Table{
   387  		Name:   `customers`,
   388  		Schema: genericChildSchema,
   389  		InitialRows: workload.BatchedTuples{
   390  			NumBatches: w.initSessions,
   391  			FillBatch:  w.childInitialRowBatchFunc(2, w.customersPerSession),
   392  		},
   393  	}
   394  	devicesTable := workload.Table{
   395  		Name:   `devices`,
   396  		Schema: deviceSchema,
   397  		InitialRows: workload.BatchedTuples{
   398  			NumBatches: w.initSessions,
   399  			FillBatch:  w.deviceInitialRowBatch,
   400  		},
   401  	}
   402  	variantsTable := workload.Table{
   403  		Name:   `variants`,
   404  		Schema: genericChildSchema,
   405  		InitialRows: workload.BatchedTuples{
   406  			NumBatches: w.initSessions,
   407  			FillBatch:  w.childInitialRowBatchFunc(3, w.variantsPerSession),
   408  		},
   409  	}
   410  	parametersTable := workload.Table{
   411  		Name:   `parameters`,
   412  		Schema: genericChildSchema,
   413  		InitialRows: workload.BatchedTuples{
   414  			NumBatches: w.initSessions,
   415  			FillBatch:  w.childInitialRowBatchFunc(4, w.parametersPerSession),
   416  		},
   417  	}
   418  	queriesTable := workload.Table{
   419  		Name:   `queries`,
   420  		Schema: querySchema,
   421  		InitialRows: workload.BatchedTuples{
   422  			NumBatches: w.initSessions,
   423  			FillBatch:  w.queryInitialRowBatch,
   424  		},
   425  	}
   426  	return []workload.Table{
   427  		sessionsTable, customerTable, devicesTable, variantsTable, parametersTable, queriesTable,
   428  	}
   429  }
   430  
   431  // Ops implements the Opser interface.
   432  func (w *interleavedPartitioned) Ops(
   433  	urls []string, reg *histogram.Registry,
   434  ) (workload.QueryLoad, error) {
   435  	sqlDatabase, err := workload.SanitizeUrls(w, ``, urls)
   436  	if err != nil {
   437  		return workload.QueryLoad{}, err
   438  	}
   439  
   440  	db, err := gosql.Open(`cockroach`, strings.Join(urls, ` `))
   441  	if err != nil {
   442  		return workload.QueryLoad{}, err
   443  	}
   444  
   445  	db.SetMaxOpenConns(w.connFlags.Concurrency + 1)
   446  	db.SetMaxIdleConns(w.connFlags.Concurrency + 1)
   447  
   448  	ql := workload.QueryLoad{
   449  		SQLDatabase: sqlDatabase,
   450  	}
   451  
   452  	workerCount := w.connFlags.Concurrency
   453  	if w.deletes {
   454  		// Only run a single worker function if performing deletes.
   455  		workerCount = 1
   456  	}
   457  
   458  	// Prepare the queries, stmts are safe for concurrent use.
   459  	w.retrieveStatements = make([]*gosql.Stmt, len(retrieveQueries))
   460  	for i, query := range retrieveQueries {
   461  		var err error
   462  		w.retrieveStatements[i], err = db.Prepare(query)
   463  		if err != nil {
   464  			return workload.QueryLoad{}, err
   465  		}
   466  	}
   467  	w.updateStatements = make([]*gosql.Stmt, len(updateQueries))
   468  	for i, query := range updateQueries {
   469  		var err error
   470  		w.updateStatements[i], err = db.Prepare(query)
   471  		if err != nil {
   472  			return workload.QueryLoad{}, err
   473  		}
   474  	}
   475  	w.insertStatement, err = db.Prepare(insertQuery)
   476  	if err != nil {
   477  		return workload.QueryLoad{}, err
   478  	}
   479  	w.insertCustomerStatement, err = db.Prepare(insertQueryCustomers)
   480  	if err != nil {
   481  		return workload.QueryLoad{}, err
   482  	}
   483  	w.insertDeviceStatement, err = db.Prepare(insertQueryDevices)
   484  	if err != nil {
   485  		return workload.QueryLoad{}, err
   486  	}
   487  	w.insertVariantStatement, err = db.Prepare(insertQueryVariants)
   488  	if err != nil {
   489  		return workload.QueryLoad{}, err
   490  	}
   491  	w.insertParameterStatement, err = db.Prepare(insertQueryParameters)
   492  	if err != nil {
   493  		return workload.QueryLoad{}, err
   494  	}
   495  	w.insertQueryStatement, err = db.Prepare(insertQueryQuery)
   496  	if err != nil {
   497  		return workload.QueryLoad{}, err
   498  	}
   499  	w.deleteEastStatement, err = db.Prepare(deleteEastQuery)
   500  	if err != nil {
   501  		return workload.QueryLoad{}, err
   502  	}
   503  	w.deleteWestStatement, err = db.Prepare(deleteWestQuery)
   504  	if err != nil {
   505  		return workload.QueryLoad{}, err
   506  	}
   507  	w.findSessionIDStatement1, err = db.Prepare(findSessionIDQuery1)
   508  	if err != nil {
   509  		return workload.QueryLoad{}, err
   510  	}
   511  	w.findSessionIDStatement2, err = db.Prepare(findSessionIDQuery2)
   512  	if err != nil {
   513  		return workload.QueryLoad{}, err
   514  	}
   515  
   516  	for i := 0; i < workerCount; i++ {
   517  		workerID := i
   518  		ql.WorkerFns = append(ql.WorkerFns, func(ctx context.Context) error {
   519  			rng := rand.New(rand.NewSource(timeutil.Now().Add(time.Hour * time.Duration(i)).UnixNano()))
   520  
   521  			hists := reg.GetHandle()
   522  			if w.deletes {
   523  				return w.deleteFunc(ctx, hists, rng)
   524  			}
   525  
   526  			operation := rng.Intn(100)
   527  			switch {
   528  			case operation < w.insertPercent: // insert
   529  				return w.insertFunc(ctx, db, hists, rng, workerID)
   530  			case operation < w.insertPercent+w.retrievePercent: // retrieve
   531  				return w.retrieveFunc(ctx, hists, rng)
   532  			case operation < w.insertPercent+w.retrievePercent+w.updatePercent: // update
   533  				return w.updateFunc(ctx, hists, rng)
   534  			default: // No operation.
   535  				return nil
   536  			}
   537  		})
   538  	}
   539  
   540  	return ql, nil
   541  }
   542  
   543  func (w *interleavedPartitioned) deleteFunc(
   544  	ctx context.Context, hists *histogram.Histograms, rng *rand.Rand,
   545  ) error {
   546  	start := timeutil.Now()
   547  	var statement *gosql.Stmt
   548  	// Prepare the statements.
   549  	if rng.Intn(2) > 0 {
   550  		statement = w.deleteEastStatement
   551  	} else {
   552  		statement = w.deleteWestStatement
   553  	}
   554  	// Execute the statements.
   555  	if _, err := statement.ExecContext(ctx, w.rowsPerDelete); err != nil {
   556  		return err
   557  	}
   558  	// Record Stats.
   559  	elapsed := timeutil.Since(start)
   560  	hists.Get(`delete`).Record(elapsed)
   561  	return nil
   562  }
   563  
   564  func (w *interleavedPartitioned) insertFunc(
   565  	ctx context.Context, db *gosql.DB, hists *histogram.Histograms, rng *rand.Rand, workerID int,
   566  ) error {
   567  	start := timeutil.Now()
   568  	// Execute the transaction.
   569  	if err := crdb.ExecuteTx(
   570  		context.Background(),
   571  		db,
   572  		nil, /* txopts */
   573  		func(tx *gosql.Tx) error {
   574  			// Get the node id.
   575  			var nodeID int
   576  			if err := tx.QueryRow(nodeIDQuery).Scan(&nodeID); err != nil {
   577  				return err
   578  			}
   579  
   580  			sessionID := randomSessionIDForInsert(rng, w.locality, w.insertLocalPercent, nodeID, workerID)
   581  			args := []interface{}{
   582  				sessionID,            // session_id
   583  				randString(rng, 100), // affiliate
   584  				randString(rng, 50),  // channel
   585  				randString(rng, 20),  // language
   586  				randString(rng, 20),  // status
   587  				randString(rng, 50),  // platform
   588  				randString(rng, 100), // query_id
   589  			}
   590  			if _, err := tx.StmtContext(ctx, w.insertStatement).ExecContext(ctx, args...); err != nil {
   591  				return err
   592  			}
   593  			for i := 0; i < w.customersPerSession; i++ {
   594  				args := []interface{}{
   595  					sessionID,           // session_id
   596  					randString(rng, 50), // id
   597  					randString(rng, 50), // value
   598  				}
   599  				if _, err := tx.StmtContext(ctx, w.insertCustomerStatement).ExecContext(ctx, args...); err != nil {
   600  					return err
   601  				}
   602  			}
   603  			for i := 0; i < w.devicesPerSession; i++ {
   604  				args := []interface{}{
   605  					sessionID,            // session_id
   606  					randString(rng, 100), // id
   607  					randString(rng, 50),  // device_id
   608  					randString(rng, 50),  // name
   609  					randString(rng, 50),  // make
   610  					randString(rng, 50),  // macaddress
   611  					randString(rng, 50),  // model
   612  					randString(rng, 50),  // serial_number
   613  				}
   614  				if _, err := tx.StmtContext(ctx, w.insertDeviceStatement).ExecContext(ctx, args...); err != nil {
   615  					return err
   616  				}
   617  			}
   618  			for i := 0; i < w.variantsPerSession; i++ {
   619  				args := []interface{}{
   620  					sessionID,           // session_id
   621  					randString(rng, 50), // id
   622  					randString(rng, 50), // value
   623  				}
   624  				if _, err := tx.StmtContext(ctx, w.insertVariantStatement).ExecContext(ctx, args...); err != nil {
   625  					return err
   626  				}
   627  			}
   628  			for i := 0; i < w.parametersPerSession; i++ {
   629  				args := []interface{}{
   630  					sessionID,           // session_id
   631  					randString(rng, 50), // id
   632  					randString(rng, 50), // value
   633  				}
   634  				if _, err := tx.StmtContext(ctx, w.insertParameterStatement).ExecContext(ctx, args...); err != nil {
   635  					return err
   636  				}
   637  			}
   638  			for i := 0; i < w.queriesPerSession; i++ {
   639  				args := []interface{}{
   640  					sessionID,           // session_id
   641  					randString(rng, 50), // id
   642  				}
   643  				if _, err := tx.StmtContext(ctx, w.insertQueryStatement).ExecContext(ctx, args...); err != nil {
   644  					return err
   645  				}
   646  			}
   647  			return nil
   648  		}); err != nil {
   649  		return err
   650  	}
   651  	// Record Stats.
   652  	elapsed := timeutil.Since(start)
   653  	hists.Get(`insert`).Record(elapsed)
   654  	return nil
   655  }
   656  
   657  func (w *interleavedPartitioned) fetchSessionID(
   658  	ctx context.Context,
   659  	rng *rand.Rand,
   660  	hists *histogram.Histograms,
   661  	locality string,
   662  	localPercent int,
   663  ) (string, error) {
   664  	start := timeutil.Now()
   665  	baseSessionID := randomSessionID(rng, locality, localPercent)
   666  	var sessionID string
   667  	if err := w.findSessionIDStatement1.QueryRowContext(ctx, baseSessionID).Scan(&sessionID); err != nil && !errors.Is(err, gosql.ErrNoRows) {
   668  		return "", err
   669  	}
   670  	// Didn't find a next session ID, let's try the other way.
   671  	if len(sessionID) == 0 {
   672  		if err := w.findSessionIDStatement2.QueryRowContext(ctx, baseSessionID).Scan(&sessionID); err != nil && !errors.Is(err, gosql.ErrNoRows) {
   673  			return "", err
   674  		}
   675  	}
   676  	elapsed := timeutil.Since(start)
   677  	if len(sessionID) == 0 {
   678  		hists.Get(`findNoID`).Record(elapsed)
   679  	} else {
   680  		hists.Get(`findID`).Record(elapsed)
   681  	}
   682  	return sessionID, nil
   683  }
   684  
   685  func (w *interleavedPartitioned) retrieveFunc(
   686  	ctx context.Context, hists *histogram.Histograms, rng *rand.Rand,
   687  ) error {
   688  	sessionID, err := w.fetchSessionID(ctx, rng, hists, w.locality, w.retrieveLocalPercent)
   689  	if err != nil {
   690  		return err
   691  	}
   692  	//Could not find a session ID, skip this operation.
   693  	if len(sessionID) == 0 {
   694  		return nil
   695  	}
   696  
   697  	start := timeutil.Now()
   698  
   699  	// Execute the queries.
   700  	for i, statement := range w.retrieveStatements {
   701  		if _, err := statement.ExecContext(ctx, sessionID); err != nil {
   702  			return errors.Wrapf(err, "error with query: %s", retrieveQueries[i])
   703  		}
   704  	}
   705  	// Record Stats.
   706  	elapsed := timeutil.Since(start)
   707  	hists.Get(`retrieve`).Record(elapsed)
   708  	return nil
   709  }
   710  
   711  func (w *interleavedPartitioned) updateFunc(
   712  	ctx context.Context, hists *histogram.Histograms, rng *rand.Rand,
   713  ) error {
   714  	sessionID, err := w.fetchSessionID(ctx, rng, hists, w.locality, w.updateLocalPercent)
   715  	if err != nil {
   716  		return err
   717  	}
   718  	//Could not find a session ID, skip this operation.
   719  	if len(sessionID) == 0 {
   720  		return nil
   721  	}
   722  
   723  	start := timeutil.Now()
   724  	// Execute the statements.
   725  	for i, statement := range w.retrieveStatements {
   726  		if _, err = statement.ExecContext(ctx, sessionID); err != nil {
   727  			return errors.Wrapf(err, "error with query: %s", retrieveQueries[i])
   728  		}
   729  	}
   730  	for i, statement := range w.updateStatements {
   731  		if _, err = statement.ExecContext(ctx, randString(rng, 20), sessionID); err != nil {
   732  			return errors.Wrapf(err, "error with query: %s", updateQueries[i])
   733  		}
   734  	}
   735  	// Record Stats.
   736  	elapsed := timeutil.Since(start)
   737  	hists.Get(`updates`).Record(elapsed)
   738  	return nil
   739  }
   740  
   741  // Hooks implements the Hookser interface.
   742  func (w *interleavedPartitioned) Hooks() workload.Hooks {
   743  	return workload.Hooks{
   744  		PreLoad: func(db *gosql.DB) error {
   745  			if _, err := db.Exec(
   746  				zoneLocationsStmt, w.eastZoneName, w.westZoneName, w.centralZoneName,
   747  			); err != nil {
   748  				return err
   749  			}
   750  			if _, err := db.Exec(
   751  				fmt.Sprintf(
   752  					"ALTER PARTITION west OF TABLE sessions CONFIGURE ZONE USING"+
   753  						" lease_preferences = '[[+zone=%[1]s]]', "+
   754  						"constraints = '{+zone=%[1]s : 1}', num_replicas = 3",
   755  					w.westZoneName,
   756  				),
   757  			); err != nil {
   758  				return errors.Wrapf(err, "could not set zone for partition west")
   759  			}
   760  			if _, err := db.Exec(
   761  				fmt.Sprintf(
   762  					"ALTER PARTITION east OF TABLE sessions CONFIGURE ZONE USING"+
   763  						" lease_preferences = '[[+zone=%[1]s]]', "+
   764  						"constraints = '{+zone=%[1]s : 1}', num_replicas = 3",
   765  					w.eastZoneName,
   766  				),
   767  			); err != nil {
   768  				return errors.Wrapf(err, "could not set zone for partition east")
   769  			}
   770  			return nil
   771  		},
   772  		Validate: func() error {
   773  			switch w.locality {
   774  			case `east`, `west`:
   775  			case `central`:
   776  				w.deletes = true
   777  				w.insertPercent = 0
   778  				w.retrievePercent = 0
   779  				w.updatePercent = 0
   780  				log.Info(context.Background(),
   781  					"locality is set to central, turning deletes on and everything else off",
   782  				)
   783  				return nil
   784  
   785  			default:
   786  				return errors.New("invalid locality (needs to be east, west, or central)")
   787  			}
   788  			if w.insertPercent+w.retrievePercent+w.updatePercent != 100 {
   789  				return errors.New(
   790  					"operation percents ({insert,retrieve,delete}-percent flags) must add up to 100",
   791  				)
   792  			}
   793  			return nil
   794  		},
   795  	}
   796  }
   797  
   798  func (w *interleavedPartitioned) sessionsInitialRow(rowIdx int) []interface{} {
   799  	rng := rand.New(rand.NewSource(int64(rowIdx)))
   800  	// Set the time for the now string to be minus 5 mins so delete operations can
   801  	// start right away.
   802  	nowString := timeutil.Now().Add(time.Minute * time.Duration(-5)).UTC().Format(time.RFC3339)
   803  	sessionID := randomSessionID(rng, `east`, w.initEastPercent)
   804  	return []interface{}{
   805  		sessionID,            // session_id
   806  		randString(rng, 100), // affiliate
   807  		randString(rng, 50),  // channel
   808  		randString(rng, 20),  // language
   809  		nowString,            // created
   810  		nowString,            // updated
   811  		randString(rng, 20),  // status
   812  		randString(rng, 50),  // platform
   813  		randString(rng, 100), // query_id
   814  	}
   815  }
   816  
   817  var childTypes = []*types.T{
   818  	types.Bytes,
   819  	types.Bytes,
   820  	types.Bytes,
   821  	types.Bytes,
   822  	types.Bytes,
   823  }
   824  
   825  func (w *interleavedPartitioned) childInitialRowBatchFunc(
   826  	rngFactor int64, nPerBatch int,
   827  ) func(int, coldata.Batch, *bufalloc.ByteAllocator) {
   828  	return func(sessionRowIdx int, cb coldata.Batch, a *bufalloc.ByteAllocator) {
   829  		sessionRNG := rand.New(rand.NewSource(int64(sessionRowIdx)))
   830  		sessionID := randomSessionID(sessionRNG, `east`, w.initEastPercent)
   831  		nowString := timeutil.Now().UTC().Format(time.RFC3339)
   832  		rng := rand.New(rand.NewSource(int64(sessionRowIdx) + rngFactor))
   833  
   834  		cb.Reset(childTypes, nPerBatch, coldata.StandardColumnFactory)
   835  		sessionIDCol := cb.ColVec(0).Bytes()
   836  		idCol := cb.ColVec(1).Bytes()
   837  		valueCol := cb.ColVec(2).Bytes()
   838  		createdCol := cb.ColVec(3).Bytes()
   839  		updatedCol := cb.ColVec(4).Bytes()
   840  		for rowIdx := 0; rowIdx < nPerBatch; rowIdx++ {
   841  			sessionIDCol.Set(rowIdx, []byte(sessionID))
   842  			idCol.Set(rowIdx, []byte(randString(rng, 50)))
   843  			valueCol.Set(rowIdx, []byte(randString(rng, 50)))
   844  			createdCol.Set(rowIdx, []byte(nowString))
   845  			updatedCol.Set(rowIdx, []byte(nowString))
   846  		}
   847  	}
   848  }
   849  
   850  var deviceTypes = []*types.T{
   851  	types.Bytes,
   852  	types.Bytes,
   853  	types.Bytes,
   854  	types.Bytes,
   855  	types.Bytes,
   856  	types.Bytes,
   857  	types.Bytes,
   858  	types.Bytes,
   859  	types.Bytes,
   860  	types.Bytes,
   861  }
   862  
   863  func (w *interleavedPartitioned) deviceInitialRowBatch(
   864  	sessionRowIdx int, cb coldata.Batch, a *bufalloc.ByteAllocator,
   865  ) {
   866  	rng := rand.New(rand.NewSource(int64(sessionRowIdx) * 64))
   867  	sessionRNG := rand.New(rand.NewSource(int64(sessionRowIdx)))
   868  	sessionID := randomSessionID(sessionRNG, `east`, w.initEastPercent)
   869  	nowString := timeutil.Now().UTC().Format(time.RFC3339)
   870  
   871  	cb.Reset(deviceTypes, w.devicesPerSession, coldata.StandardColumnFactory)
   872  	sessionIDCol := cb.ColVec(0).Bytes()
   873  	idCol := cb.ColVec(1).Bytes()
   874  	deviceIDCol := cb.ColVec(2).Bytes()
   875  	nameCol := cb.ColVec(3).Bytes()
   876  	makeCol := cb.ColVec(4).Bytes()
   877  	macaddressCol := cb.ColVec(5).Bytes()
   878  	modelCol := cb.ColVec(6).Bytes()
   879  	serialNumberCol := cb.ColVec(7).Bytes()
   880  	createdCol := cb.ColVec(8).Bytes()
   881  	updatedCol := cb.ColVec(9).Bytes()
   882  	for rowIdx := 0; rowIdx < w.devicesPerSession; rowIdx++ {
   883  		sessionIDCol.Set(rowIdx, []byte(sessionID))
   884  		idCol.Set(rowIdx, []byte(randString(rng, 100)))
   885  		deviceIDCol.Set(rowIdx, []byte(randString(rng, 50)))
   886  		nameCol.Set(rowIdx, []byte(randString(rng, 50)))
   887  		makeCol.Set(rowIdx, []byte(randString(rng, 50)))
   888  		macaddressCol.Set(rowIdx, []byte(randString(rng, 50)))
   889  		modelCol.Set(rowIdx, []byte(randString(rng, 50)))
   890  		serialNumberCol.Set(rowIdx, []byte(randString(rng, 50)))
   891  		createdCol.Set(rowIdx, []byte(nowString))
   892  		updatedCol.Set(rowIdx, []byte(nowString))
   893  	}
   894  }
   895  
   896  var queryTypes = []*types.T{
   897  	types.Bytes,
   898  	types.Bytes,
   899  	types.Bytes,
   900  	types.Bytes,
   901  }
   902  
   903  func (w *interleavedPartitioned) queryInitialRowBatch(
   904  	sessionRowIdx int, cb coldata.Batch, a *bufalloc.ByteAllocator,
   905  ) {
   906  	rng := rand.New(rand.NewSource(int64(sessionRowIdx) * 64))
   907  	sessionRNG := rand.New(rand.NewSource(int64(sessionRowIdx)))
   908  	sessionID := randomSessionID(sessionRNG, `east`, w.initEastPercent)
   909  	nowString := timeutil.Now().UTC().Format(time.RFC3339)
   910  
   911  	cb.Reset(queryTypes, w.queriesPerSession, coldata.StandardColumnFactory)
   912  	sessionIDCol := cb.ColVec(0).Bytes()
   913  	idCol := cb.ColVec(1).Bytes()
   914  	createdCol := cb.ColVec(2).Bytes()
   915  	updatedCol := cb.ColVec(3).Bytes()
   916  	for rowIdx := 0; rowIdx < w.queriesPerSession; rowIdx++ {
   917  		sessionIDCol.Set(rowIdx, []byte(sessionID))
   918  		idCol.Set(rowIdx, []byte(randString(rng, 50)))
   919  		createdCol.Set(rowIdx, []byte(nowString))
   920  		updatedCol.Set(rowIdx, []byte(nowString))
   921  	}
   922  }
   923  
   924  func randomSessionID(rng *rand.Rand, locality string, localPercent int) string {
   925  	return randomSessionIDForInsert(rng, locality, localPercent, 0 /* nodeID */, 0 /* workerID */)
   926  }
   927  
   928  func randomSessionIDForInsert(
   929  	rng *rand.Rand, locality string, localPercent int, nodeID int, workerID int,
   930  ) string {
   931  	// Is this a local operation? As in an east node accessing east data.
   932  	local := rng.Intn(100) < localPercent
   933  	// There have been some issues of session ID collisions so by adding the node
   934  	// and worker IDs is an attempt to minimize that. If they still occur, it must
   935  	// point to a serious issue and having the IDs should help identify it.
   936  	if (local && locality == `east`) || (!local && locality == `west`) {
   937  		return fmt.Sprintf("E-%s-n%dw%d", randString(rng, 90), nodeID, workerID)
   938  	}
   939  	return fmt.Sprintf("W-%s-n%dw%d", randString(rng, 90), nodeID, workerID)
   940  }
   941  
   942  func randString(rng *rand.Rand, length int) string {
   943  	return string(randutil.RandBytes(rng, length))
   944  }