vitess.io/vitess@v0.16.2/go/test/endtoend/onlineddl/vrepl_stress_suite/onlineddl_vrepl_stress_suite_test.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /*
    18  	This endtoend suite tests VReplication based Online DDL under stress (concurrent INSERT/UPDATE/DELETE queries),
    19  	and looks for before/after table data to be identical.
    20  	This suite specifically targets choice of unique key: PRIMARY vs non-PRIMARY, numeric, textual, compound.
    21  	The scenarios caused by this suite cause VReplication to iterate the table in:
    22  	- expected incremental order (id)
    23  	- expected decremental order (negative id values)
    24  	- random order (random textual checksums)
    25  	- other
    26  */
    27  
    28  package vreplstress
    29  
    30  import (
    31  	"context"
    32  	"flag"
    33  	"fmt"
    34  	"math/rand"
    35  	"os"
    36  	"path"
    37  	"strings"
    38  	"sync"
    39  	"sync/atomic"
    40  	"testing"
    41  	"time"
    42  
    43  	"vitess.io/vitess/go/mysql"
    44  	"vitess.io/vitess/go/timer"
    45  	"vitess.io/vitess/go/vt/log"
    46  	"vitess.io/vitess/go/vt/schema"
    47  
    48  	"vitess.io/vitess/go/test/endtoend/cluster"
    49  	"vitess.io/vitess/go/test/endtoend/onlineddl"
    50  
    51  	"github.com/stretchr/testify/assert"
    52  	"github.com/stretchr/testify/require"
    53  )
    54  
    55  type testcase struct {
    56  	// name is a human readable name for the test
    57  	name string
    58  	// prepareStatement modifies the table (direct strategy) before it gets populated
    59  	prepareStatement string
    60  	// alterStatement is the online statement used in the test
    61  	alterStatement string
    62  	// expectFailure is typically false. If true then we do not compare table data results
    63  	expectFailure bool
    64  	// expectAddedUniqueKeys is the number of added constraints
    65  	expectAddedUniqueKeys int64
    66  	// expectRemovedUniqueKeys is the number of alleviated constraints
    67  	expectRemovedUniqueKeys int64
    68  	// autoIncInsert is a special case where we don't generate id values. It's a specific test case.
    69  	autoIncInsert bool
    70  }
    71  
    72  var (
    73  	clusterInstance      *cluster.LocalProcessCluster
    74  	vtParams             mysql.ConnParams
    75  	evaluatedMysqlParams *mysql.ConnParams
    76  
    77  	directDDLStrategy     = "direct"
    78  	onlineDDLStrategy     = "vitess -vreplication-test-suite -skip-topo"
    79  	hostname              = "localhost"
    80  	keyspaceName          = "ks"
    81  	cell                  = "zone1"
    82  	shards                []cluster.Shard
    83  	opOrder               int64
    84  	opOrderMutex          sync.Mutex
    85  	schemaChangeDirectory = ""
    86  	tableName             = "stress_test"
    87  	afterTableName        = "stress_test_after"
    88  	cleanupStatements     = []string{
    89  		`DROP TABLE IF EXISTS stress_test`,
    90  		`DROP TABLE IF EXISTS stress_test_before`,
    91  		`DROP TABLE IF EXISTS stress_test_after`,
    92  	}
    93  	createStatement = `
    94  		CREATE TABLE stress_test (
    95  			id bigint not null,
    96  			id_negative bigint not null,
    97  			rand_text varchar(40) not null default '',
    98  			rand_num bigint unsigned not null,
    99  			nullable_num int default null,
   100  			op_order bigint unsigned not null default 0,
   101  			hint_col varchar(64) not null default '',
   102  			created_timestamp timestamp not null default current_timestamp,
   103  			updates int unsigned not null default 0,
   104  			PRIMARY KEY (id),
   105  			KEY id_idx(id)
   106  		) ENGINE=InnoDB
   107  	`
   108  	testCases = []testcase{
   109  		{
   110  			name:             "trivial PK",
   111  			prepareStatement: "",
   112  			alterStatement:   "engine=innodb",
   113  		},
   114  		{
   115  			name:             "autoinc PK",
   116  			prepareStatement: "modify id bigint not null auto_increment",
   117  			alterStatement:   "engine=innodb",
   118  			autoIncInsert:    true,
   119  		},
   120  		{
   121  			name:             "UK similar to PK, no PK",
   122  			prepareStatement: "add unique key id_uidx(id)",
   123  			alterStatement:   "drop primary key",
   124  		},
   125  		{
   126  			name:             "negative PK",
   127  			prepareStatement: "drop primary key, add primary key (id_negative)",
   128  			alterStatement:   "engine=innodb",
   129  		},
   130  		{
   131  			name:                    "negative UK, no PK",
   132  			prepareStatement:        "add unique key negative_uidx(id_negative)",
   133  			alterStatement:          "drop primary key",
   134  			expectRemovedUniqueKeys: 1,
   135  		},
   136  		{
   137  			name:                    "negative UK, different PK",
   138  			prepareStatement:        "add unique key negative_uidx(id_negative)",
   139  			alterStatement:          "drop primary key, add primary key(rand_text(40))",
   140  			expectAddedUniqueKeys:   1,
   141  			expectRemovedUniqueKeys: 1,
   142  		},
   143  		{
   144  			name:                    "text UK, no PK",
   145  			prepareStatement:        "add unique key text_uidx(rand_text(40))",
   146  			alterStatement:          "drop primary key",
   147  			expectRemovedUniqueKeys: 1,
   148  		},
   149  		{
   150  			name:                    "text UK, different PK",
   151  			prepareStatement:        "add unique key text_uidx(rand_text(40))",
   152  			alterStatement:          "drop primary key, add primary key (id, id_negative)",
   153  			expectRemovedUniqueKeys: 1,
   154  		},
   155  		{
   156  			name:                    "compound UK 1 by text, no PK",
   157  			prepareStatement:        "add unique key compound_uidx(rand_text(40), id_negative)",
   158  			alterStatement:          "drop primary key",
   159  			expectRemovedUniqueKeys: 1,
   160  		},
   161  		{
   162  			name:                    "compound UK 2 by negative, no PK",
   163  			prepareStatement:        "add unique key compound_uidx(id_negative, rand_text(40))",
   164  			alterStatement:          "drop primary key",
   165  			expectRemovedUniqueKeys: 1,
   166  		},
   167  		{
   168  			name:                    "compound UK 3 by ascending int, no PK",
   169  			prepareStatement:        "add unique key compound_uidx(id, rand_num, rand_text(40))",
   170  			alterStatement:          "drop primary key",
   171  			expectRemovedUniqueKeys: 1,
   172  		},
   173  		{
   174  			name:                    "compound UK 4 by rand int, no PK",
   175  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40))",
   176  			alterStatement:          "drop primary key",
   177  			expectRemovedUniqueKeys: 1,
   178  		},
   179  		{
   180  			name:                    "compound UK 5 by rand int, different PK",
   181  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40))",
   182  			alterStatement:          "drop primary key, add primary key (id, id_negative)",
   183  			expectRemovedUniqueKeys: 1,
   184  		},
   185  		{
   186  			name:                    "multiple UK choices 1",
   187  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40)), add unique key negative_uidx(id_negative)",
   188  			alterStatement:          "drop primary key, add primary key(updates, id)",
   189  			expectRemovedUniqueKeys: 1,
   190  		},
   191  		{
   192  			name:                    "multiple UK choices 2",
   193  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40)), add unique key negative_uidx(id_negative)",
   194  			alterStatement:          "drop primary key, add primary key(id, id_negative)",
   195  			expectRemovedUniqueKeys: 1,
   196  		},
   197  		{
   198  			name:                    "multiple UK choices including nullable with PK",
   199  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40)), add unique key nullable_uidx(nullable_num, id_negative), add unique key negative_uidx(id_negative)",
   200  			alterStatement:          "drop primary key, drop key negative_uidx, add primary key(id_negative)",
   201  			expectRemovedUniqueKeys: 1,
   202  		},
   203  		{
   204  			name:                    "multiple UK choices including nullable",
   205  			prepareStatement:        "add unique key compound_uidx(rand_num, rand_text(40)), add unique key nullable_uidx(nullable_num, id_negative), add unique key negative_uidx(id_negative)",
   206  			alterStatement:          "drop primary key, add primary key(updates, id)",
   207  			expectRemovedUniqueKeys: 1,
   208  		},
   209  		{
   210  			name:                    "different PRIMARY KEY",
   211  			prepareStatement:        "",
   212  			alterStatement:          "drop primary key, add primary key(id_negative)",
   213  			expectAddedUniqueKeys:   1,
   214  			expectRemovedUniqueKeys: 1,
   215  		},
   216  		{
   217  			name:                    "extended PRIMARY KEY",
   218  			prepareStatement:        "",
   219  			alterStatement:          "drop primary key, add primary key(id, id_negative)",
   220  			expectRemovedUniqueKeys: 1,
   221  		},
   222  		{
   223  			name:                    "extended PRIMARY KEY, different order",
   224  			prepareStatement:        "",
   225  			alterStatement:          "drop primary key, add primary key(id_negative, id)",
   226  			expectRemovedUniqueKeys: 1,
   227  		},
   228  		{
   229  			name:                  "reduced PRIMARY KEY",
   230  			prepareStatement:      "drop primary key, add primary key(id, id_negative)",
   231  			alterStatement:        "drop primary key, add primary key(id)",
   232  			expectAddedUniqueKeys: 1,
   233  		},
   234  		{
   235  			name:                  "reduced PRIMARY KEY 2",
   236  			prepareStatement:      "drop primary key, add primary key(id, id_negative)",
   237  			alterStatement:        "drop primary key, add primary key(id_negative)",
   238  			expectAddedUniqueKeys: 1,
   239  		},
   240  		{
   241  			name:                    "different PRIMARY KEY, text",
   242  			prepareStatement:        "",
   243  			alterStatement:          "drop primary key, add primary key(rand_text(40))",
   244  			expectAddedUniqueKeys:   1,
   245  			expectRemovedUniqueKeys: 1,
   246  		},
   247  		{
   248  			name:                    "different PRIMARY KEY, rand",
   249  			prepareStatement:        "",
   250  			alterStatement:          "drop primary key, add primary key(rand_num, rand_text(40))",
   251  			expectAddedUniqueKeys:   1,
   252  			expectRemovedUniqueKeys: 1,
   253  		},
   254  		{
   255  			name:                    "different PRIMARY KEY, from negative to int",
   256  			prepareStatement:        "drop primary key, add primary key(id_negative)",
   257  			alterStatement:          "drop primary key, add primary key(id)",
   258  			expectAddedUniqueKeys:   1,
   259  			expectRemovedUniqueKeys: 1,
   260  		},
   261  		{
   262  			name:                    "different PRIMARY KEY, from text to int",
   263  			prepareStatement:        "drop primary key, add primary key(rand_text(40))",
   264  			alterStatement:          "drop primary key, add primary key(id)",
   265  			expectAddedUniqueKeys:   1,
   266  			expectRemovedUniqueKeys: 1,
   267  		},
   268  		{
   269  			name:                    "different PRIMARY KEY, from text to rand",
   270  			prepareStatement:        "drop primary key, add primary key(rand_text(40))",
   271  			alterStatement:          "drop primary key, add primary key(rand_num, rand_text(40))",
   272  			expectRemovedUniqueKeys: 1,
   273  		},
   274  		{
   275  			name:                    "partially shared PRIMARY KEY 1",
   276  			prepareStatement:        "drop primary key, add primary key(id, id_negative)",
   277  			alterStatement:          "drop primary key, add primary key(id, rand_text(40))",
   278  			expectAddedUniqueKeys:   1,
   279  			expectRemovedUniqueKeys: 1,
   280  		},
   281  		{
   282  			name:                    "partially shared PRIMARY KEY 2",
   283  			prepareStatement:        "drop primary key, add primary key(id, id_negative)",
   284  			alterStatement:          "drop primary key, add primary key(id_negative, rand_text(40))",
   285  			expectAddedUniqueKeys:   1,
   286  			expectRemovedUniqueKeys: 1,
   287  		},
   288  		{
   289  			name:                    "partially shared PRIMARY KEY 3",
   290  			prepareStatement:        "drop primary key, add primary key(id, id_negative)",
   291  			alterStatement:          "drop primary key, add primary key(rand_text(40), id)",
   292  			expectAddedUniqueKeys:   1,
   293  			expectRemovedUniqueKeys: 1,
   294  		},
   295  		{
   296  			name:                    "partially shared PRIMARY KEY 4",
   297  			prepareStatement:        "drop primary key, add primary key(id_negative, id)",
   298  			alterStatement:          "drop primary key, add primary key(rand_text(40), id)",
   299  			expectAddedUniqueKeys:   1,
   300  			expectRemovedUniqueKeys: 1,
   301  		},
   302  		{
   303  			name:                    "different PRIMARY KEY vs UNIQUE KEY",
   304  			prepareStatement:        "",
   305  			alterStatement:          "drop primary key, add unique key(id_negative)",
   306  			expectAddedUniqueKeys:   1,
   307  			expectRemovedUniqueKeys: 1,
   308  		},
   309  		{
   310  			name:                    "no shared UK, multiple options",
   311  			prepareStatement:        "add unique key negative_uidx(id_negative)",
   312  			alterStatement:          "drop primary key, drop key negative_uidx, add primary key(rand_text(40)), add unique key negtext_uidx(id_negative, rand_text(40))",
   313  			expectAddedUniqueKeys:   1,
   314  			expectRemovedUniqueKeys: 2,
   315  		},
   316  		{
   317  			name:             "fail; no uk on target",
   318  			prepareStatement: "",
   319  			alterStatement:   "drop primary key",
   320  			expectFailure:    true,
   321  		},
   322  		{
   323  			name:             "fail; only nullable shared uk",
   324  			prepareStatement: "add unique key nullable_uidx(nullable_num)",
   325  			alterStatement:   "drop primary key",
   326  			expectFailure:    true,
   327  		},
   328  	}
   329  	alterHintStatement = `
   330  		alter table stress_test modify hint_col varchar(64) not null default '%s'
   331  	`
   332  
   333  	insertRowAutoIncStatement = `
   334  		INSERT IGNORE INTO stress_test (id, id_negative, rand_text, rand_num, op_order) VALUES (NULL, %d, concat(left(md5(%d), 8), '_', %d), floor(rand()*1000000), %d)
   335  	`
   336  	insertRowStatement = `
   337  		INSERT IGNORE INTO stress_test (id, id_negative, rand_text, rand_num, op_order) VALUES (%d, %d, concat(left(md5(%d), 8), '_', %d), floor(rand()*1000000), %d)
   338  	`
   339  	updateRowStatement = `
   340  		UPDATE stress_test SET op_order=%d, updates=updates+1 WHERE id=%d
   341  	`
   342  	deleteRowStatement = `
   343  		DELETE FROM stress_test WHERE id=%d
   344  	`
   345  	selectCountFromTable = `
   346  		SELECT count(*) as c FROM stress_test
   347  	`
   348  	selectCountFromTableBefore = `
   349  		SELECT count(*) as c FROM stress_test_before
   350  	`
   351  	selectCountFromTableAfter = `
   352  		SELECT count(*) as c FROM stress_test_after
   353  	`
   354  	selectMaxOpOrderFromTableBefore = `
   355  		SELECT MAX(op_order) as m FROM stress_test_before
   356  	`
   357  	selectMaxOpOrderFromTableAfter = `
   358  		SELECT MAX(op_order) as m FROM stress_test_after
   359  	`
   360  	selectBeforeTable = `
   361  		SELECT * FROM stress_test_before order by id, id_negative, rand_text, rand_num
   362  	`
   363  	selectAfterTable = `
   364  		SELECT * FROM stress_test_after order by id, id_negative, rand_text, rand_num
   365  	`
   366  	truncateStatement = `
   367  		TRUNCATE TABLE stress_test
   368  	`
   369  )
   370  
   371  const (
   372  	maxTableRows                  = 4096
   373  	maxConcurrency                = 15
   374  	singleConnectionSleepInterval = 5 * time.Millisecond
   375  	periodicSleepPercent          = 10 // in the range (0,100). 10 means 10% sleep time throught the stress load.
   376  	waitForStatusTimeout          = 180 * time.Second
   377  )
   378  
   379  func resetOpOrder() {
   380  	opOrderMutex.Lock()
   381  	defer opOrderMutex.Unlock()
   382  	opOrder = 0
   383  }
   384  
   385  func nextOpOrder() int64 {
   386  	opOrderMutex.Lock()
   387  	defer opOrderMutex.Unlock()
   388  	opOrder++
   389  	return opOrder
   390  }
   391  
   392  func getTablet() *cluster.Vttablet {
   393  	return clusterInstance.Keyspaces[0].Shards[0].Vttablets[0]
   394  }
   395  
   396  func mysqlParams() *mysql.ConnParams {
   397  	if evaluatedMysqlParams != nil {
   398  		return evaluatedMysqlParams
   399  	}
   400  	evaluatedMysqlParams = &mysql.ConnParams{
   401  		Uname:      "vt_dba",
   402  		UnixSocket: path.Join(os.Getenv("VTDATAROOT"), fmt.Sprintf("/vt_%010d", getTablet().TabletUID), "/mysql.sock"),
   403  		DbName:     fmt.Sprintf("vt_%s", keyspaceName),
   404  	}
   405  	return evaluatedMysqlParams
   406  }
   407  
   408  func TestMain(m *testing.M) {
   409  	defer cluster.PanicHandler(nil)
   410  	flag.Parse()
   411  
   412  	exitcode, err := func() (int, error) {
   413  		clusterInstance = cluster.NewCluster(cell, hostname)
   414  		schemaChangeDirectory = path.Join("/tmp", fmt.Sprintf("schema_change_dir_%d", clusterInstance.GetAndReserveTabletUID()))
   415  		defer os.RemoveAll(schemaChangeDirectory)
   416  		defer clusterInstance.Teardown()
   417  
   418  		if _, err := os.Stat(schemaChangeDirectory); os.IsNotExist(err) {
   419  			_ = os.Mkdir(schemaChangeDirectory, 0700)
   420  		}
   421  
   422  		clusterInstance.VtctldExtraArgs = []string{
   423  			"--schema_change_dir", schemaChangeDirectory,
   424  			"--schema_change_controller", "local",
   425  			"--schema_change_check_interval", "1",
   426  		}
   427  
   428  		// --vstream_packet_size is set to a small value that ensures we get multiple stream iterations,
   429  		// thereby examining lastPK on vcopier side. We will be iterating tables using non-PK order throughout
   430  		// this test suite, and so the low setting ensures we hit the more interesting code paths.
   431  		clusterInstance.VtTabletExtraArgs = []string{
   432  			"--enable-lag-throttler",
   433  			"--throttle_threshold", "1s",
   434  			"--heartbeat_enable",
   435  			"--heartbeat_interval", "250ms",
   436  			"--heartbeat_on_demand_duration", "5s",
   437  			"--migration_check_interval", "5s",
   438  			"--vstream_packet_size", "4096", // Keep this value small and below 10k to ensure multilple vstream iterations
   439  			"--watch_replication_stream",
   440  		}
   441  		clusterInstance.VtGateExtraArgs = []string{
   442  			"--ddl_strategy", "online",
   443  		}
   444  
   445  		if err := clusterInstance.StartTopo(); err != nil {
   446  			return 1, err
   447  		}
   448  
   449  		// Start keyspace
   450  		keyspace := &cluster.Keyspace{
   451  			Name: keyspaceName,
   452  		}
   453  
   454  		// No need for replicas in this stress test
   455  		if err := clusterInstance.StartKeyspace(*keyspace, []string{"1"}, 0, false); err != nil {
   456  			return 1, err
   457  		}
   458  
   459  		vtgateInstance := clusterInstance.NewVtgateInstance()
   460  		// Start vtgate
   461  		if err := vtgateInstance.Setup(); err != nil {
   462  			return 1, err
   463  		}
   464  		// ensure it is torn down during cluster TearDown
   465  		clusterInstance.VtgateProcess = *vtgateInstance
   466  		vtParams = mysql.ConnParams{
   467  			Host: clusterInstance.Hostname,
   468  			Port: clusterInstance.VtgateMySQLPort,
   469  		}
   470  
   471  		return m.Run(), nil
   472  	}()
   473  	if err != nil {
   474  		fmt.Printf("%v\n", err)
   475  		os.Exit(1)
   476  	} else {
   477  		os.Exit(exitcode)
   478  	}
   479  
   480  }
   481  
   482  func TestSchemaChange(t *testing.T) {
   483  	defer cluster.PanicHandler(t)
   484  
   485  	shards = clusterInstance.Keyspaces[0].Shards
   486  	require.Equal(t, 1, len(shards))
   487  
   488  	for _, testcase := range testCases {
   489  		require.NotEmpty(t, testcase.name)
   490  		t.Run(testcase.name, func(t *testing.T) {
   491  			t.Run("cancel pending migrations", func(t *testing.T) {
   492  				cancelQuery := "alter vitess_migration cancel all"
   493  				r := onlineddl.VtgateExecQuery(t, &vtParams, cancelQuery, "")
   494  				if r.RowsAffected > 0 {
   495  					fmt.Printf("# Cancelled migrations (for debug purposes): %d\n", r.RowsAffected)
   496  				}
   497  			})
   498  			t.Run("create schema", func(t *testing.T) {
   499  				assert.Equal(t, 1, len(clusterInstance.Keyspaces[0].Shards))
   500  				testWithInitialSchema(t)
   501  			})
   502  			t.Run("prepare table", func(t *testing.T) {
   503  				if testcase.prepareStatement != "" {
   504  					fullStatement := fmt.Sprintf("alter table %s %s", tableName, testcase.prepareStatement)
   505  					onlineddl.VtgateExecDDL(t, &vtParams, directDDLStrategy, fullStatement, "")
   506  				}
   507  			})
   508  			t.Run("init table data", func(t *testing.T) {
   509  				initTable(t)
   510  			})
   511  			t.Run("migrate", func(t *testing.T) {
   512  				require.NotEmpty(t, testcase.alterStatement)
   513  
   514  				hintText := fmt.Sprintf("hint-after-alter-%d", rand.Int31n(int32(maxTableRows)))
   515  				hintStatement := fmt.Sprintf(alterHintStatement, hintText)
   516  				fullStatement := fmt.Sprintf("%s, %s", hintStatement, testcase.alterStatement)
   517  
   518  				ctx, cancel := context.WithCancel(context.Background())
   519  				var wg sync.WaitGroup
   520  				wg.Add(1)
   521  				go func() {
   522  					defer wg.Done()
   523  					runMultipleConnections(ctx, t, testcase.autoIncInsert)
   524  				}()
   525  				uuid := testOnlineDDLStatement(t, fullStatement, onlineDDLStrategy, "vtgate", hintText)
   526  				expectStatus := schema.OnlineDDLStatusComplete
   527  				if testcase.expectFailure {
   528  					expectStatus = schema.OnlineDDLStatusFailed
   529  				}
   530  				status := onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, waitForStatusTimeout, expectStatus)
   531  				fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
   532  				onlineddl.CheckMigrationStatus(t, &vtParams, shards, uuid, expectStatus)
   533  				cancel() // will cause runMultipleConnections() to terminate
   534  				wg.Wait()
   535  				if !testcase.expectFailure {
   536  					testCompareBeforeAfterTables(t, testcase.autoIncInsert)
   537  				}
   538  
   539  				rs := onlineddl.ReadMigrations(t, &vtParams, uuid)
   540  				for _, row := range rs.Named().Rows {
   541  					assert.Equal(t, testcase.expectAddedUniqueKeys, row.AsInt64("added_unique_keys", 0), "expectAddedUniqueKeys")
   542  					assert.Equal(t, testcase.expectRemovedUniqueKeys, row.AsInt64("removed_unique_keys", 0), "expectRemovedUniqueKeys")
   543  				}
   544  			})
   545  		})
   546  	}
   547  }
   548  
   549  func testWithInitialSchema(t *testing.T) {
   550  	// Create the stress table
   551  	for _, statement := range cleanupStatements {
   552  		err := clusterInstance.VtctlclientProcess.ApplySchema(keyspaceName, statement)
   553  		require.Nil(t, err)
   554  	}
   555  	err := clusterInstance.VtctlclientProcess.ApplySchema(keyspaceName, createStatement)
   556  	require.Nil(t, err)
   557  
   558  	// Check if table is created
   559  	checkTable(t, tableName)
   560  }
   561  
   562  // testOnlineDDLStatement runs an online DDL, ALTER statement
   563  func testOnlineDDLStatement(t *testing.T, alterStatement string, ddlStrategy string, executeStrategy string, expectHint string) (uuid string) {
   564  	if executeStrategy == "vtgate" {
   565  		row := onlineddl.VtgateExecDDL(t, &vtParams, ddlStrategy, alterStatement, "").Named().Row()
   566  		if row != nil {
   567  			uuid = row.AsString("uuid", "")
   568  		}
   569  	} else {
   570  		var err error
   571  		uuid, err = clusterInstance.VtctlclientProcess.ApplySchemaWithOutput(keyspaceName, alterStatement, cluster.VtctlClientParams{DDLStrategy: ddlStrategy})
   572  		assert.NoError(t, err)
   573  	}
   574  	uuid = strings.TrimSpace(uuid)
   575  	fmt.Println("# Generated UUID (for debug purposes):")
   576  	fmt.Printf("<%s>\n", uuid)
   577  
   578  	strategySetting, err := schema.ParseDDLStrategy(ddlStrategy)
   579  	assert.NoError(t, err)
   580  
   581  	status := schema.OnlineDDLStatusComplete
   582  	if !strategySetting.Strategy.IsDirect() {
   583  		status = onlineddl.WaitForMigrationStatus(t, &vtParams, shards, uuid, waitForStatusTimeout, schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed)
   584  		fmt.Printf("# Migration status (for debug purposes): <%s>\n", status)
   585  	}
   586  
   587  	if expectHint != "" && status == schema.OnlineDDLStatusComplete {
   588  		checkMigratedTable(t, afterTableName, expectHint)
   589  	}
   590  	return uuid
   591  }
   592  
   593  // checkTable checks the number of tables in the first two shards.
   594  func checkTable(t *testing.T, showTableName string) {
   595  	for i := range clusterInstance.Keyspaces[0].Shards {
   596  		checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], showTableName, 1)
   597  	}
   598  }
   599  
   600  // checkTablesCount checks the number of tables in the given tablet
   601  func checkTablesCount(t *testing.T, tablet *cluster.Vttablet, showTableName string, expectCount int) {
   602  	query := fmt.Sprintf(`show tables like '%%%s%%';`, showTableName)
   603  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   604  	defer cancel()
   605  	rowcount := 0
   606  	for {
   607  		queryResult, err := tablet.VttabletProcess.QueryTablet(query, keyspaceName, true)
   608  		require.Nil(t, err)
   609  		rowcount = len(queryResult.Rows)
   610  		if rowcount > 0 {
   611  			break
   612  		}
   613  
   614  		select {
   615  		case <-time.After(time.Second):
   616  		case <-ctx.Done():
   617  			break
   618  		}
   619  	}
   620  	assert.Equal(t, expectCount, rowcount)
   621  }
   622  
   623  // checkMigratedTables checks the CREATE STATEMENT of a table after migration
   624  func checkMigratedTable(t *testing.T, tableName, expectHint string) {
   625  	for i := range clusterInstance.Keyspaces[0].Shards {
   626  		createStatement := getCreateTableStatement(t, clusterInstance.Keyspaces[0].Shards[i].Vttablets[0], tableName)
   627  		assert.Contains(t, createStatement, expectHint)
   628  	}
   629  }
   630  
   631  // getCreateTableStatement returns the CREATE TABLE statement for a given table
   632  func getCreateTableStatement(t *testing.T, tablet *cluster.Vttablet, tableName string) (statement string) {
   633  	queryResult, err := tablet.VttabletProcess.QueryTablet(fmt.Sprintf("show create table %s;", tableName), keyspaceName, true)
   634  	require.Nil(t, err)
   635  
   636  	assert.Equal(t, len(queryResult.Rows), 1)
   637  	assert.Equal(t, len(queryResult.Rows[0]), 2) // table name, create statement
   638  	statement = queryResult.Rows[0][1].ToString()
   639  	return statement
   640  }
   641  
   642  func generateInsert(t *testing.T, conn *mysql.Conn, autoIncInsert bool) error {
   643  	id := rand.Int31n(int32(maxTableRows))
   644  	query := fmt.Sprintf(insertRowStatement, id, -id, id, id, nextOpOrder())
   645  	if autoIncInsert {
   646  		id = rand.Int31()
   647  		query = fmt.Sprintf(insertRowAutoIncStatement, -id, id, id, nextOpOrder())
   648  	}
   649  	qr, err := conn.ExecuteFetch(query, 1000, true)
   650  	if err == nil && qr != nil {
   651  		assert.Less(t, qr.RowsAffected, uint64(2))
   652  	}
   653  	return err
   654  }
   655  
   656  func generateUpdate(t *testing.T, conn *mysql.Conn) error {
   657  	id := rand.Int31n(int32(maxTableRows))
   658  	query := fmt.Sprintf(updateRowStatement, nextOpOrder(), id)
   659  	qr, err := conn.ExecuteFetch(query, 1000, true)
   660  	if err == nil && qr != nil {
   661  		assert.Less(t, qr.RowsAffected, uint64(2))
   662  	}
   663  	return err
   664  }
   665  
   666  func generateDelete(t *testing.T, conn *mysql.Conn) error {
   667  	id := rand.Int31n(int32(maxTableRows))
   668  	query := fmt.Sprintf(deleteRowStatement, id)
   669  	qr, err := conn.ExecuteFetch(query, 1000, true)
   670  	if err == nil && qr != nil {
   671  		assert.Less(t, qr.RowsAffected, uint64(2))
   672  	}
   673  	return err
   674  }
   675  
   676  func runSingleConnection(ctx context.Context, t *testing.T, autoIncInsert bool, done *int64) {
   677  	log.Infof("Running single connection")
   678  	conn, err := mysql.Connect(ctx, &vtParams)
   679  	require.Nil(t, err)
   680  	defer conn.Close()
   681  
   682  	_, err = conn.ExecuteFetch("set autocommit=1", 1, false)
   683  	require.Nil(t, err)
   684  	_, err = conn.ExecuteFetch("set transaction isolation level read committed", 1, false)
   685  	require.Nil(t, err)
   686  	_, err = conn.ExecuteFetch("set innodb_lock_wait_timeout=1", 1, false)
   687  	require.Nil(t, err)
   688  
   689  	periodicRest := timer.NewRateLimiter(time.Second)
   690  	defer periodicRest.Stop()
   691  	for {
   692  		if atomic.LoadInt64(done) == 1 {
   693  			log.Infof("Terminating single connection")
   694  			return
   695  		}
   696  		switch rand.Int31n(3) {
   697  		case 0:
   698  			err = generateInsert(t, conn, autoIncInsert)
   699  		case 1:
   700  			err = generateUpdate(t, conn)
   701  		case 2:
   702  			err = generateDelete(t, conn)
   703  		}
   704  		if err != nil {
   705  			if strings.Contains(err.Error(), "doesn't exist") {
   706  				// Table renamed to _before, due to -vreplication-test-suite flag
   707  				err = nil
   708  			}
   709  			if sqlErr, ok := err.(*mysql.SQLError); ok {
   710  				switch sqlErr.Number() {
   711  				case mysql.ERLockDeadlock:
   712  					// That's fine. We create a lot of contention; some transactions will deadlock and
   713  					// rollback. It happens, and we can ignore those and keep on going.
   714  					err = nil
   715  				}
   716  			}
   717  		}
   718  		assert.Nil(t, err)
   719  		time.Sleep(singleConnectionSleepInterval)
   720  		// Most o fthe time, we want the load to be high, so as to create real stress and potentially
   721  		// expose bugs in vreplication (the objective of this test!).
   722  		// However, some platforms (GitHub CI) can suffocate from this load. We choose to keep the load
   723  		// high, when it runs, but then also take a periodic break and let the system recover.
   724  		// We prefer this over reducing the load in general. In our method here, we have full load 90% of
   725  		// the time, then relaxation 10% of the time.
   726  		periodicRest.Do(func() error {
   727  			time.Sleep(time.Second * periodicSleepPercent / 100)
   728  			return nil
   729  		})
   730  	}
   731  }
   732  
   733  func runMultipleConnections(ctx context.Context, t *testing.T, autoIncInsert bool) {
   734  	log.Infof("Running multiple connections")
   735  	var done int64
   736  	var wg sync.WaitGroup
   737  	for i := 0; i < maxConcurrency; i++ {
   738  		wg.Add(1)
   739  		go func() {
   740  			defer wg.Done()
   741  			runSingleConnection(ctx, t, autoIncInsert, &done)
   742  		}()
   743  	}
   744  	<-ctx.Done()
   745  	atomic.StoreInt64(&done, 1)
   746  	log.Infof("Running multiple connections: done")
   747  	wg.Wait()
   748  	log.Infof("All connections cancelled")
   749  }
   750  
   751  func initTable(t *testing.T) {
   752  	log.Infof("initTable begin")
   753  	defer log.Infof("initTable complete")
   754  
   755  	ctx := context.Background()
   756  	conn, err := mysql.Connect(ctx, &vtParams)
   757  	require.Nil(t, err)
   758  	defer conn.Close()
   759  
   760  	resetOpOrder()
   761  
   762  	_, err = conn.ExecuteFetch(truncateStatement, 1000, true)
   763  	require.Nil(t, err)
   764  
   765  	for i := 0; i < maxTableRows/2; i++ {
   766  		generateInsert(t, conn, false)
   767  	}
   768  	for i := 0; i < maxTableRows/4; i++ {
   769  		generateUpdate(t, conn)
   770  	}
   771  	for i := 0; i < maxTableRows/4; i++ {
   772  		generateDelete(t, conn)
   773  	}
   774  	{
   775  		// Validate table is populated
   776  		rs, err := conn.ExecuteFetch(selectCountFromTable, 1000, true)
   777  		require.Nil(t, err)
   778  		row := rs.Named().Row()
   779  		require.NotNil(t, row)
   780  
   781  		count := row.AsInt64("c", 0)
   782  		require.NotZero(t, count)
   783  		require.Less(t, count, int64(maxTableRows))
   784  
   785  		fmt.Printf("# count rows in table: %d\n", count)
   786  	}
   787  }
   788  
   789  // testCompareBeforeAfterTables validates that stress_test_before and stress_test_after contents are non empty and completely identical
   790  func testCompareBeforeAfterTables(t *testing.T, autoIncInsert bool) {
   791  	var countBefore int64
   792  	{
   793  		// Validate after table is populated
   794  		rs := onlineddl.VtgateExecQuery(t, &vtParams, selectCountFromTableBefore, "")
   795  		row := rs.Named().Row()
   796  		require.NotNil(t, row)
   797  
   798  		countBefore = row.AsInt64("c", 0)
   799  		require.NotZero(t, countBefore)
   800  		if !autoIncInsert {
   801  			require.Less(t, countBefore, int64(maxTableRows))
   802  		}
   803  		fmt.Printf("# count rows in table (before): %d\n", countBefore)
   804  	}
   805  	var countAfter int64
   806  	{
   807  		// Validate after table is populated
   808  		rs := onlineddl.VtgateExecQuery(t, &vtParams, selectCountFromTableAfter, "")
   809  		row := rs.Named().Row()
   810  		require.NotNil(t, row)
   811  
   812  		countAfter = row.AsInt64("c", 0)
   813  		require.NotZero(t, countAfter)
   814  		if !autoIncInsert {
   815  			require.Less(t, countAfter, int64(maxTableRows))
   816  		}
   817  		fmt.Printf("# count rows in table (after): %d\n", countAfter)
   818  	}
   819  	{
   820  		rs := onlineddl.VtgateExecQuery(t, &vtParams, selectMaxOpOrderFromTableBefore, "")
   821  		row := rs.Named().Row()
   822  		require.NotNil(t, row)
   823  
   824  		maxOpOrder := row.AsInt64("m", 0)
   825  		fmt.Printf("# max op_order in table (before): %d\n", maxOpOrder)
   826  	}
   827  	{
   828  		rs := onlineddl.VtgateExecQuery(t, &vtParams, selectMaxOpOrderFromTableAfter, "")
   829  		row := rs.Named().Row()
   830  		require.NotNil(t, row)
   831  
   832  		maxOpOrder := row.AsInt64("m", 0)
   833  		fmt.Printf("# max op_order in table (after): %d\n", maxOpOrder)
   834  	}
   835  
   836  	{
   837  		selectBeforeFile := onlineddl.CreateTempScript(t, selectBeforeTable)
   838  		defer os.Remove(selectBeforeFile)
   839  		beforeOutput := onlineddl.MysqlClientExecFile(t, mysqlParams(), os.TempDir(), "", selectBeforeFile)
   840  		beforeOutput = strings.TrimSpace(beforeOutput)
   841  		require.NotEmpty(t, beforeOutput)
   842  		assert.Equal(t, countBefore, int64(len(strings.Split(beforeOutput, "\n"))))
   843  
   844  		selectAfterFile := onlineddl.CreateTempScript(t, selectAfterTable)
   845  		defer os.Remove(selectAfterFile)
   846  		afterOutput := onlineddl.MysqlClientExecFile(t, mysqlParams(), os.TempDir(), "", selectAfterFile)
   847  		afterOutput = strings.TrimSpace(afterOutput)
   848  		require.NotEmpty(t, afterOutput)
   849  		assert.Equal(t, countAfter, int64(len(strings.Split(afterOutput, "\n"))))
   850  
   851  		require.Equal(t, beforeOutput, afterOutput, "results mismatch: (%s) and (%s)", selectBeforeTable, selectAfterTable)
   852  	}
   853  }